<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="generator" content="VNote">

    <title>16_基于接口的客户端-服务端服务</title>
    <link rel="icon" href="https://github.com/tamlok/vnote/raw/master/src/resources/icons/vnote.ico">

    <style type="text/css">
    /* STYLE_GLOBAL_PLACE_HOLDER */
    </style>

    <style type="text/css">
    *,
*::before,
*::after {
  box-sizing: border-box;
}

.container-fluid {
    width: 100%;
    padding-right: 15px;
    padding-left: 15px;
    margin-right: auto;
    margin-left: auto;
}

.col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto {
    position: relative;
    width: 100%;
    min-height: 1px;
    padding-right: 15px;
    padding-left: 15px;
}

.col-12 {
    -webkit-box-flex: 0;
    -ms-flex: 0 0 100%;
    flex: 0 0 100%;
    max-width: 100%;
}

@media (min-width: 768px) {
    .col-md-3 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 25%;
        flex: 0 0 25%;
        max-width: 25%;
    }
}

@media (min-width: 768px) {
    .col-md-9 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 75%;
        flex: 0 0 75%;
        max-width: 75%;
    }
}

@media (min-width: 1200px) {
    .col-xl-2 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 16.666667%;
        flex: 0 0 16.666667%;
        max-width: 16.666667%;
    }
}

@media (min-width: 1200px) {
    .col-xl-10 {
        -webkit-box-flex: 0;
        -ms-flex: 0 0 83.333333%;
        flex: 0 0 83.333333%;
        max-width: 83.333333%;
    }
}

@media (min-width: 768px) {
    .pt-md-3, .py-md-3 {
        padding-top: 1rem!important;
    }
}

@media (min-width: 768px) {
    .pb-md-3, .py-md-3 {
        padding-bottom: 1rem!important;
    }
}

@media (min-width: 768px) {
    .pl-md-5, .px-md-5 {
        padding-left: 3rem!important;
    }
}

.d-none {
    display: none!important;
}

@media (min-width: 1200px) {
    .d-xl-block {
        display: block!important;
    }
}

@media (min-width: 768px) {
    .d-md-block {
        display: block!important;
    }
}

.bd-content {
    -webkit-box-ordinal-group: 1;
    -ms-flex-order: 0;
    order: 0;
}

.bd-toc {
    position: -webkit-sticky;
    position: sticky;
    top: 4rem;
    height: calc(100vh - 10rem);
    overflow-y: auto;
}

.bd-toc {
    -webkit-box-ordinal-group: 2;
    -ms-flex-order: 1;
    order: 1;
    padding-top: 1.5rem;
    padding-bottom: 1.5rem;
    font-size: .875rem;
}

.section-nav {
    padding-left: 0;
}

.section-nav ul {
    font-size: .875rem;
    list-style-type: none;
}

.section-nav li {
    font-size: .875rem;
}

.section-nav a {
    color: inherit !important;
}

.row {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-wrap: wrap;
    flex-wrap: wrap;
    margin-right: -15px;
    margin-left: -15px;
}

@media (min-width: 1200px) {
    .flex-xl-nowrap {
        flex-wrap: nowrap !important;
    }
}

#floating-button {
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 50%;
    background: #00897B;
    position: fixed;
    top: .5rem;
    right: .5rem;
    cursor: pointer;
    box-shadow: 0px 2px 5px #666;
}

#floating-button .more {
    color: #F5F5F5;
    position: absolute;
    top: 0;
    display: block;
    bottom: 0;
    left: 0;
    right: 0;
    text-align: center;
    padding: 0;
    margin: 0;
    line-height: 2.5rem;
    font-size: 2rem;
    font-family: 'monospace';
    font-weight: 300;
}

.hide-none {
    display: none !important;
}

.col-expand {
    -webkit-box-flex: 0;
    -ms-flex: 0 0 100% !important;
    flex: 0 0 100% !important;
    max-width: 100% !important;
    padding-right: 3rem !important;
}

.outline-bold {
    font-weight: bolder !important;
}

@media print {
    #floating-button {
        display: none !important;
    }
}

    @keyframes flash { 
  0% { color: rgb(128, 203, 196); }
  10% { color: rgb(0, 137, 123); }
  40% { color: rgb(0, 137, 123); }
  50% { color: rgb(128, 203, 196); }
  60% { color: rgb(0, 137, 123); }
  90% { color: rgb(0, 137, 123); }
}
.highlighted-anchor { animation: flash 1s; }
div.mark-rect { background: transparent; border: 5px solid rgb(87, 104, 196); border-radius: 2px; position: absolute; }
#vnote-footer { width: 100%; text-align: center; opacity: 0.2; margin-top: 3rem; }
#vnote-footer p { font-size: 0.8rem; }
#vnote-footer a { color: inherit !important; }
x-eqs { display: flex; flex-direction: row; align-content: space-between; align-items: center; }
x-eqs > x-eqn { width: 100%; margin-left: 3rem; }
x-eqs > span { text-align: right; }
.view-image, .view-svg { transition: 0.3s; }
.modal-box { display: none; position: fixed; z-index: 1000; padding-top: 50px; left: 0px; top: 0px; width: 100%; height: 100%; overflow: hidden; background-color: rgba(68, 68, 68, 0.952941); }
.modal-content { margin: auto; display: block; width: auto; height: auto; cursor: move; }
.modal-content { animation-name: zoom; animation-duration: 0.6s; }
@-webkit-keyframes zoom { 
  0% { transform: scale(0); }
  100% { transform: scale(1); }
}
@keyframes zoom { 
  0% { transform: scale(0); }
  100% { transform: scale(1); }
}
span.modal-close { position: absolute; z-index: 1000; top: 15px; right: 35px; color: rgb(218, 218, 218); font-size: 40px; font-weight: bold; transition: 0.3s; }
span.modal-close:hover, span.modal-close:focus { color: rgb(238, 238, 238); text-decoration: none; cursor: pointer; }
@media print {
  pre, pre code, td.hljs-ln-code { white-space: pre-wrap !important; word-break: break-all !important; }
  code, a { word-break: break-all !important; }
  div.flowchart-diagram, div.mermaid-diagram, div.plantuml-diagram { overflow: hidden !important; }
  img { max-width: 100% !important; height: auto !important; }
  #vnote-footer { display: none !important; }
}
.alert { position: relative; padding: 0.75rem 1.25rem; margin-bottom: 1rem; border: 1px solid transparent; border-radius: 0.25rem; }
.alert-primary { color: rgb(0, 64, 133); background-color: rgb(204, 229, 255); border-color: rgb(184, 218, 255); }
.alert-secondary { color: rgb(56, 61, 65); background-color: rgb(226, 227, 229); border-color: rgb(214, 216, 219); }
.alert-success { color: rgb(21, 87, 36); background-color: rgb(212, 237, 218); border-color: rgb(195, 230, 203); }
.alert-info { color: rgb(12, 84, 96); background-color: rgb(209, 236, 241); border-color: rgb(190, 229, 235); }
.alert-warning { color: rgb(133, 100, 4); background-color: rgb(255, 243, 205); border-color: rgb(255, 238, 186); }
.alert-danger { color: rgb(114, 28, 36); background-color: rgb(248, 215, 218); border-color: rgb(245, 198, 203); }
.alert-light { color: rgb(129, 129, 130); background-color: rgb(254, 254, 254); border-color: rgb(253, 253, 254); }
.alert-dark { color: rgb(27, 30, 33); background-color: rgb(214, 216, 217); border-color: rgb(198, 200, 202); }
.vnote-anchor { font-weight: 400; color: rgba(0, 123, 255, 0.498039); transition: color 0.16s linear; padding-left: 0.375em; -webkit-font-smoothing: antialiased; text-decoration: none; opacity: 0; }
.vnote-anchor:hover { color: rgb(0, 123, 255); text-decoration: none; opacity: 1; }
.vnote-anchor::after { content: attr(data-anchor-icon); }
.vnote-btn { position: relative; display: inline-block; padding: 6px 12px; font-size: 13px; font-weight: 700; line-height: 20px; white-space: nowrap; vertical-align: middle; cursor: pointer; border: none; user-select: none; -webkit-appearance: none; }
.vnote-copy-clipboard-btn { transition: opacity 0.3s ease-in-out; opacity: 0; padding: 2px 6px; position: absolute; top: 5px; right: 5px; }
pre:hover .vnote-copy-clipboard-btn { opacity: 1; }
pre.vnote-snippet { position: relative; }
body { margin: 0px auto; font-family: "Segoe UI", Helvetica, sans-serif, Tahoma, Arial, Geneva, Georgia, Palatino, "Times New Roman", "Hiragino Sans GB", 冬青黑体, "Microsoft YaHei", 微软雅黑, "Microsoft YaHei UI", "WenQuanYi Micro Hei", 文泉驿雅黑, Dengxian, 等线体, STXihei, 华文细黑, "Liberation Sans", "Droid Sans", NSimSun, 新宋体, SimSun, 宋体; color: rgb(34, 34, 34); line-height: 1.5; padding: 15px; background: rgb(238, 238, 238); font-size: 16px; }
h1, h2, h3, h4, h5, h6 { color: rgb(34, 34, 34); font-weight: bold; margin-top: 20px; margin-bottom: 10px; padding: 0px; }
p { padding: 0px; margin-top: 16px; margin-bottom: 16px; }
h1 { font-size: 26px; }
h2 { font-size: 24px; }
h3 { font-size: 22px; }
h4 { font-size: 20px; }
h5 { font-size: 19px; }
h6 { font-size: 18px; }
a { color: rgb(0, 153, 255); margin: 0px; padding: 0px; vertical-align: baseline; text-decoration: none; word-break: break-word; }
a:hover { text-decoration: underline; color: rgb(255, 102, 0); }
a:visited { color: purple; }
ul, ol { padding: 0px 0px 0px 24px; }
li { line-height: 24px; }
li ul, li ol { margin-left: 16px; }
p, ul, ol { font-size: 16px; line-height: 24px; }
pre { display: block; overflow-y: hidden; overflow-x: auto; tab-size: 4; }
code { font-family: Consolas, Monaco, monospace, Courier; color: rgb(142, 36, 170); word-break: break-word; }
pre code { display: block; overflow-x: auto; padding: 0.5em; color: rgb(34, 34, 34); background-color: rgb(224, 224, 224); border-left: 0.5em solid rgb(0, 137, 123); line-height: 1.5; font-family: Consolas, Monaco, monospace, Courier; white-space: pre; tab-size: 4; }
pre code.markdown-metadata { border-left: 0.5em solid rgb(128, 203, 196); }
aside { display: block; float: right; width: 390px; }
blockquote { color: rgb(102, 102, 102); border-left: 0.5em solid rgb(122, 122, 122); padding: 0px 1em; margin-left: 0px; }
blockquote p { color: rgb(102, 102, 102); }
hr { display: block; text-align: left; margin: 1em 0px; border: none; height: 2px; background: rgb(153, 153, 153); }
table { padding: 0px; margin: 1rem 0.5rem; border-collapse: collapse; }
table tr { border-top: 2px solid rgb(204, 204, 204); background-color: white; margin: 0px; padding: 0px; }
table tr:nth-child(2n) { background-color: rgb(248, 248, 248); }
table tr th { font-weight: bold; border: 2px solid rgb(204, 204, 204); margin: 0px; padding: 6px 13px; }
table tr td { border: 2px solid rgb(204, 204, 204); margin: 0px; padding: 6px 13px; }
table tr th :first-child, table tr td :first-child { margin-top: 0px; }
table tr th :last-child, table tr td :last-child { margin-bottom: 0px; }
div.mermaid-diagram { margin: 16px 0px; overflow-y: hidden; }
div.flowchart-diagram { padding: 0px 5px; margin: 16px 0px; width: fit-content; overflow: hidden; }
div.wavedrom-diagram { padding: 0px 5px; margin: 16px 0px; width: fit-content; overflow: hidden; }
div.plantuml-diagram { padding: 5px 5px 0px; margin: 16px 0px; width: fit-content; overflow: hidden; }
.img-package { text-align: center; }
img.img-center { display: block; margin-left: auto; margin-right: auto; }
span.img-caption { min-width: 20%; max-width: 80%; display: inline-block; padding: 10px; margin: 0px auto; border-bottom: 1px solid rgb(192, 192, 192); color: rgb(108, 108, 108); text-align: center; line-height: 1.5; }
.emoji_zero, .emoji_one, .emoji_two, .emoji_three, .emoji_four, .emoji_five, .emoji_six, .emoji_seven, .emoji_eight, .emoji_nine { margin-left: 5px; margin-right: 8px; }
div.preview-hint { opacity: 0.5; margin-top: 30%; margin-bottom: 30%; align-items: center; display: flex; flex-direction: column; justify-content: center; }
table.hljs-ln tr { border: none; background-color: transparent; }
table.hljs-ln tr td { border: none; background-color: transparent; }
table.hljs-ln tr td.hljs-ln-numbers { user-select: none; text-align: center; color: rgb(170, 170, 170); border-right: 1px solid rgb(204, 204, 204); vertical-align: top; padding-right: 5px; white-space: nowrap; }
table.hljs-ln tr td.hljs-ln-code { padding-left: 10px; }
::-webkit-scrollbar { background-color: rgb(234, 234, 234); width: 14px; height: 14px; border: none; }
::-webkit-scrollbar-corner { background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-button { height: 14px; width: 14px; background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-button:hover { background-color: rgb(208, 208, 208); }
::-webkit-scrollbar-button:active { background-color: rgb(178, 178, 178); }
::-webkit-scrollbar-track { background-color: rgb(234, 234, 234); }
::-webkit-scrollbar-thumb { border: none; background-color: rgb(218, 218, 218); }
::-webkit-scrollbar-thumb:hover { background-color: rgb(208, 208, 208); }
::-webkit-scrollbar-thumb:active { background-color: rgb(178, 178, 178); }
::-webkit-scrollbar-button:horizontal:increment { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(-90 256.00000000000006,256) " id="svg_1">   <polygon fill="%23333333" id="svg_2" points="128,192 256,320 384,192  "/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:horizontal:decrement { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(90 255.99999999999997,256.00000000000006) " id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:vertical:increment { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="null" id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::-webkit-scrollbar-button:vertical:decrement { background-image: url('data:image/svg+xml;utf8,<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> <g>    <g transform="rotate(180 255.99999999999997,256) " id="svg_1">   <polygon points="128,192 256,320 384,192  " id="svg_2" fill="%23333333"/>  </g> </g></svg>'); background-repeat: no-repeat; background-size: contain; }
::selection { background: rgb(25, 118, 210); color: rgb(238, 238, 238); }
.modal-box { background-color: rgba(234, 234, 234, 0.952941); }
span.modal-close { color: rgb(102, 102, 102); }
span.modal-close:hover, span.modal-close:focus { color: rgb(34, 34, 34); }
.hljs { display: block; overflow-x: auto; padding: 0.5em; background: rgb(224, 224, 224); }
.hljs, .hljs-subst { color: rgb(54, 54, 54); }
.hljs-comment { color: rgb(118, 118, 118); }
.hljs-keyword, .hljs-attribute, .hljs-selector-tag, .hljs-meta-keyword, .hljs-doctag, .hljs-name { color: rgb(0, 0, 238); }
.hljs-type, .hljs-string, .hljs-number, .hljs-selector-id, .hljs-selector-class, .hljs-quote, .hljs-template-tag, .hljs-deletion { color: rgb(136, 0, 0); }
.hljs-title, .hljs-section { color: rgb(136, 0, 0); font-weight: bold; }
.hljs-regexp, .hljs-symbol, .hljs-variable, .hljs-template-variable, .hljs-link, .hljs-selector-attr, .hljs-selector-pseudo { color: rgb(188, 96, 96); }
.hljs-literal { color: rgb(175, 0, 215); }
.hljs-built_in, .hljs-bullet, .hljs-code, .hljs-addition { color: rgb(0, 135, 0); }
.hljs-meta { color: rgb(31, 113, 153); }
.hljs-meta-string { color: rgb(77, 153, 191); }
.hljs-emphasis { font-style: italic; }
.hljs-strong { font-weight: bold; }
.mermaid-diagram .mermaid .label { color: rgb(51, 51, 51); }
.mermaid-diagram .node rect, .mermaid-diagram .node circle, .mermaid-diagram .node ellipse, .mermaid-diagram .node polygon { fill: rgb(236, 236, 255); stroke: rgb(204, 204, 255); stroke-width: 1px; }
.mermaid-diagram .edgePath .path { stroke: rgb(51, 51, 51); }
.mermaid-diagram .edgeLabel { background-color: rgb(232, 232, 232); }
.mermaid-diagram .cluster rect { fill: rgb(255, 255, 222) !important; rx: 4 !important; stroke: rgb(170, 170, 51) !important; stroke-width: 1px !important; }
.mermaid-diagram .cluster text { fill: rgb(51, 51, 51); }
.mermaid-diagram .actor { stroke: rgb(204, 204, 255); fill: rgb(236, 236, 255); }
.mermaid-diagram text.actor { fill: black; stroke: none; }
.mermaid-diagram .actor-line { stroke: grey; }
.mermaid-diagram .messageLine0 { stroke-width: 1.5; stroke: rgb(51, 51, 51); }
.mermaid-diagram .messageLine1 { stroke-width: 1.5; stroke: rgb(51, 51, 51); }
.mermaid-diagram #arrowhead { fill: rgb(51, 51, 51); }
.mermaid-diagram #crosshead path { fill: rgb(51, 51, 51) !important; stroke: rgb(51, 51, 51) !important; }
.mermaid-diagram .messageText { fill: rgb(51, 51, 51); stroke: none; }
.mermaid-diagram .labelBox { stroke: rgb(204, 204, 255); fill: rgb(236, 236, 255); }
.mermaid-diagram .labelText { fill: black; stroke: none; }
.mermaid-diagram .loopText { fill: black; stroke: none; }
.mermaid-diagram .loopLine { stroke-width: 2; stroke: rgb(204, 204, 255); }
.mermaid-diagram .note { stroke: rgb(170, 170, 51); fill: rgb(255, 245, 173); }
.mermaid-diagram .noteText { fill: black; stroke: none; font-family: "trebuchet ms", verdana, arial; font-size: 14px; }
.mermaid-diagram .section { stroke: none; opacity: 0.2; }
.mermaid-diagram .section0 { fill: rgba(102, 102, 255, 0.490196); }
.mermaid-diagram .section2 { fill: rgb(255, 244, 0); }
.mermaid-diagram .section1, .mermaid-diagram .section3 { fill: white; opacity: 0.2; }
.mermaid-diagram .sectionTitle0 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle1 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle2 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle3 { fill: rgb(51, 51, 51); }
.mermaid-diagram .sectionTitle { text-anchor: start; font-size: 11px; }
.mermaid-diagram .grid .tick { stroke: lightgrey; opacity: 0.3; shape-rendering: crispEdges; }
.mermaid-diagram .grid path { stroke-width: 0; }
.mermaid-diagram .today { fill: none; stroke: red; stroke-width: 2px; }
.mermaid-diagram .task { stroke-width: 2; }
.mermaid-diagram .taskText { text-anchor: middle; font-size: 11px; }
.mermaid-diagram .taskTextOutsideRight { fill: black; text-anchor: start; font-size: 11px; }
.mermaid-diagram .taskTextOutsideLeft { fill: black; text-anchor: end; font-size: 11px; }
.mermaid-diagram .taskText0, .mermaid-diagram .taskText1, .mermaid-diagram .taskText2, .mermaid-diagram .taskText3 { fill: white; }
.mermaid-diagram .task0, .mermaid-diagram .task1, .mermaid-diagram .task2, .mermaid-diagram .task3 { fill: rgb(138, 144, 221); stroke: rgb(83, 79, 188); }
.mermaid-diagram .taskTextOutside0, .mermaid-diagram .taskTextOutside2 { fill: black; }
.mermaid-diagram .taskTextOutside1, .mermaid-diagram .taskTextOutside3 { fill: black; }
.mermaid-diagram .active0, .mermaid-diagram .active1, .mermaid-diagram .active2, .mermaid-diagram .active3 { fill: rgb(191, 199, 255); stroke: rgb(83, 79, 188); }
.mermaid-diagram .activeText0, .mermaid-diagram .activeText1, .mermaid-diagram .activeText2, .mermaid-diagram .activeText3 { fill: black !important; }
.mermaid-diagram .done0, .mermaid-diagram .done1, .mermaid-diagram .done2, .mermaid-diagram .done3 { stroke: grey; fill: lightgrey; stroke-width: 2; }
.mermaid-diagram .doneText0, .mermaid-diagram .doneText1, .mermaid-diagram .doneText2, .mermaid-diagram .doneText3 { fill: black !important; }
.mermaid-diagram .crit0, .mermaid-diagram .crit1, .mermaid-diagram .crit2, .mermaid-diagram .crit3 { stroke: rgb(255, 136, 136); fill: red; stroke-width: 2; }
.mermaid-diagram .activeCrit0, .mermaid-diagram .activeCrit1, .mermaid-diagram .activeCrit2, .mermaid-diagram .activeCrit3 { stroke: rgb(255, 136, 136); fill: rgb(191, 199, 255); stroke-width: 2; }
.mermaid-diagram .doneCrit0, .mermaid-diagram .doneCrit1, .mermaid-diagram .doneCrit2, .mermaid-diagram .doneCrit3 { stroke: rgb(255, 136, 136); fill: lightgrey; stroke-width: 2; cursor: pointer; shape-rendering: crispEdges; }
.mermaid-diagram .doneCritText0, .mermaid-diagram .doneCritText1, .mermaid-diagram .doneCritText2, .mermaid-diagram .doneCritText3 { fill: black !important; }
.mermaid-diagram .activeCritText0, .mermaid-diagram .activeCritText1, .mermaid-diagram .activeCritText2, .mermaid-diagram .activeCritText3 { fill: black !important; }
.mermaid-diagram .titleText { text-anchor: middle; font-size: 18px; fill: black; }
.mermaid-diagram .node text { font-family: "trebuchet ms", verdana, arial; font-size: 14px; }
.mermaid-diagram div.mermaidTooltip { position: absolute; text-align: center; max-width: 200px; padding: 2px; font-family: "trebuchet ms", verdana, arial; font-size: 12px; background: rgb(255, 255, 222); border: 1px solid rgb(170, 170, 51); border-radius: 2px; pointer-events: none; z-index: 100; }
#mermaid-diagram-1 .node > rect { }
#mermaid-diagram-1 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-1 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-1 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-1 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-2 .node > rect { }
#mermaid-diagram-2 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-2 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-2 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-2 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-3 .node > rect { }
#mermaid-diagram-3 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-3 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-3 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-3 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }
#mermaid-diagram-4 .node > rect { }
#mermaid-diagram-4 .node text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-4 .edgeLabel text { fill: rgb(0, 0, 0); stroke: none; font-weight: 300; font-family: "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 14px; }
#mermaid-diagram-4 .cluster rect { fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-4 .cyan > rect, .cyan > polygon, .cyan > circle, .cyan > ellipse { fill: rgb(153, 255, 255); stroke: rgb(255, 255, 255); }

    </style>

    <script type="text/javascript">
var toc = [];

var setVisible = function(node, visible) {
    var cl = 'hide-none';
    if (visible) {
        node.classList.remove(cl);
    } else {
        node.classList.add(cl);
    }
};

var isVisible = function(node) {
    var cl = 'hide-none';
    return !node.classList.contains(cl);
};

var setPostContentExpanded = function(node, expanded) {
    var cl = 'col-expand';
    if (expanded) {
        node.classList.add(cl);
    } else {
        node.classList.remove(cl);
    }
};

var setOutlinePanelVisible = function(visible) {
    var outlinePanel = document.getElementById('outline-panel');
    var postContent = document.getElementById('post-content');

    setVisible(outlinePanel, visible);
    setPostContentExpanded(postContent, !visible);
};

var isOutlinePanelVisible = function() {
    var outlinePanel = document.getElementById('outline-panel');
    return isVisible(outlinePanel);
};

window.addEventListener('load', function() {
    var outlinePanel = document.getElementById('outline-panel');
    outlinePanel.style.display = 'initial';

    var floatingContainer = document.getElementById('container-floating');
    floatingContainer.style.display = 'initial';

    var outlineContent = document.getElementById('outline-content');
    var postContent = document.getElementById('post-content');

    // Escape @text to Html.
    var escapeHtml = function(text) {
        var map = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#039;'
        };

        return text.replace(/[&<>"']/g, function(m) { return map[m]; });
    }

    // Fetch the outline.
    var headers = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6");
    toc = [];
    for (var i = 0; i < headers.length; ++i) {
        var header = headers[i];

        toc.push({
            level: parseInt(header.tagName.substr(1)),
            anchor: header.id,
            title: escapeHtml(header.textContent)
        });
    }

    if (toc.length == 0) {
        setOutlinePanelVisible(false);
        setVisible(floatingContainer, false);
        return;
    }

    var baseLevel = baseLevelOfToc(toc);
    var tocTree = tocToTree(toPerfectToc(toc, baseLevel), baseLevel);

    outlineContent.innerHTML = tocTree;
    setOutlinePanelVisible(true);
    setVisible(floatingContainer, true);
});

// Return the topest level of @toc, starting from 1.
var baseLevelOfToc = function(p_toc) {
    var level = -1;
    for (i in p_toc) {
        if (level == -1) {
            level = p_toc[i].level;
        } else if (level > p_toc[i].level) {
            level = p_toc[i].level;
        }
    }

    if (level == -1) {
        level = 1;
    }

    return level;
};

// Handle wrong title levels, such as '#' followed by '###'
var toPerfectToc = function(p_toc, p_baseLevel) {
    var i;
    var curLevel = p_baseLevel - 1;
    var perfToc = [];
    for (i in p_toc) {
        var item = p_toc[i];

        // Insert empty header.
        while (item.level > curLevel + 1) {
            curLevel += 1;
            var tmp = { level: curLevel,
                        anchor: '',
                        title: '[EMPTY]'
                      };
            perfToc.push(tmp);
        }

        perfToc.push(item);
        curLevel = item.level;
    }

    return perfToc;
};

var itemToHtml = function(item) {
    return '<a href="#' + item.anchor + '" data="' + item.anchor + '">' + item.title + '</a>';
};

// Turn a perfect toc to a tree using <ul>
var tocToTree = function(p_toc, p_baseLevel) {
    var i;
    var front = '<li>';
    var ending = ['</li>'];
    var curLevel = p_baseLevel;
    for (i in p_toc) {
        var item = p_toc[i];
        if (item.level == curLevel) {
            front += '</li>';
            front += '<li>';
            front += itemToHtml(item);
        } else if (item.level > curLevel) {
            // assert(item.level - curLevel == 1)
            front += '<ul>';
            ending.push('</ul>');
            front += '<li>';
            front += itemToHtml(item);
            ending.push('</li>');
            curLevel = item.level;
        } else {
            while (item.level < curLevel) {
                var ele = ending.pop();
                front += ele;
                if (ele == '</ul>') {
                    curLevel--;
                }
            }
            front += '</li>';
            front += '<li>';
            front += itemToHtml(item);
        }
    }
    while (ending.length > 0) {
        front += ending.pop();
    }
    front = front.replace("<li></li>", "");
    front = '<ul>' + front + '</ul>';
    return front;
};

var toggleMore = function() {
    if (toc.length == 0) {
        return;
    }

    var p = document.getElementById('floating-more');
    if (isOutlinePanelVisible()) {
        p.textContent = '<';
        setOutlinePanelVisible(false);
    } else {
        p.textContent = '>';
        setOutlinePanelVisible(true);
    }
};

window.addEventListener('scroll', function() {
    if (toc.length == 0 || !isOutlinePanelVisible()) {
        return;
    }

    var postContent = document.getElementById('post-content');
    var scrollTop = document.documentElement.scrollTop
                    || document.body.scrollTop
                    || window.pageYOffset;
    var eles = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6");

    if (eles.length == 0) {
        return;
    }

    var idx = -1;
    var biaScrollTop = scrollTop + 50;
    for (var i = 0; i < eles.length; ++i) {
        if (biaScrollTop >= eles[i].offsetTop) {
            idx = i;
        } else {
            break;
        }
    }

    var header = '';
    if (idx != -1) {
        header = eles[idx].id;
    }

    highlightItemOnlyInOutline(header);
});

var highlightItemOnlyInOutline = function(id) {
    var cl = 'outline-bold';
    var outlineContent = document.getElementById('outline-content');
    var eles = outlineContent.querySelectorAll("a");
    var target = null;
    for (var i = 0; i < eles.length; ++i) {
        var ele = eles[i];
        if (ele.getAttribute('data') == id) {
            target = ele;
            ele.classList.add(cl);
        } else {
            ele.classList.remove(cl);
        }
    }

    // TODO: scroll target into view within the outline panel scroll area.
};

</script>


<!-- HEAD_PLACE_HOLDER -->
</head>
<body>
<div class="container-fluid">
<div class="row flex-xl-nowrap">
    <div id="outline-panel" style="display:none;" class="d-none d-md-block d-xl-block col-md-3 col-xl-2 bd-toc">
        <div id="outline-content" class="section-nav"></div>
    </div>
    <div id="post-content" class="col-12 col-md-9 col-xl-10 py-md-3 pl-md-5 bd-content">
    <div style="page-break-after: always;"></div>
<h1 id="toc_0">16. 基于接口的客户端-服务端服务<a class="vnote-anchor" href="#toc_0" data-anchor-icon="#"></a></h1>
<p><img src='' alt="" class="view-image"></p>
<p>  在现实世界中，特别是当您的应用程序严重依赖服务时，通过方法实现模式的客户端-服务端服务有一些缺点：</p>
<ul>
<li>大多数内容编码都是手工完成的，因此可能会导致实施问题；</li>
<li>客户端和服务端代码没有相同的实现模式，因此您必须为客户端和服务端显式编码两次数据编组（DataSnap和WCF都遇到类似的问题，客户端类需单独编码，通常用向导生成）;</li>
<li>除非您编写大量代码来模拟“虚假”服务实现，否则您无法轻松测试您的服务；</li>
<li>服务没有任何层次结构，为普通列表，这很不方便；</li>
<li>在单个上下文中很难同步多个服务调用，如在应用程序进程中处理工作流时（必须在两侧编写某种状态机，并手动定义所有会话处理）；</li>
<li>全局处理用户安全性，或者需要在实现方法中手动检查（使用<code>Ctxt.Session*</code>成员）；</li>
<li>无法实现服务回调，如使用WebSockets。</li>
</ul>
<p>  您可以通过mORMot的基于接口的服务实现摆脱这些限制。有关SOA的详细介绍和最佳实践指南，请参阅<a href="">面向服务的体系结构（SOA）</a>。所有通常需要的SOA功能现在都可以在mORMot框架的当前实现中获得（包括服务目录，也称“代理”，通过可选的接口签名发布）。</p>
<h2 id="toc_1">16.1. 实现特性<a class="vnote-anchor" href="#toc_1" data-anchor-icon="#"></a></h2>
<p>  以下是使用Synopse mORMot框架当前服务接口实现的主要特性，在<code>mORMot.pas</code>单元中实现：</p>
<table>
<thead>
<tr>
<th>功能</th>
<th>摘要</th>
</tr>
</thead>
<tbody>
<tr>
<td>面向服务</td>
<td>允许松耦合关系</td>
</tr>
<tr>
<td>契约式设计</td>
<td>服务契约在Delphi代码中定义为标准自定义接口类型</td>
</tr>
<tr>
<td>驱动工厂</td>
<td>从给定接口获取实现实例</td>
</tr>
<tr>
<td>服务端工厂</td>
<td>您可以在服务器端获得实现</td>
</tr>
<tr>
<td>客户端工厂</td>
<td>您可以在客户端获得一个“伪”实现，远程服务端调用来执行流程</td>
</tr>
<tr>
<td>跨平台客户端</td>
<td>mORMot服务端能够通过一组模板生成跨平台的客户端代码</td>
</tr>
<tr>
<td>自动编码</td>
<td>该契约是透明实现的：不需要额外的代码，例如在客户端，它将处理简单类型（字符串，数字，日期，集合和枚举）和高级类型（对象，集合，记录，动态数组，变体），支持Delphi 6到Delphi 10.3 Rio</td>
</tr>
<tr>
<td>灵活</td>
<td>方法接受每个值或每个引用参数</td>
</tr>
<tr>
<td>实例的生命周期</td>
<td>实现类可以是:<br>- 每次调用创建，<br>- 所有调用共享，<br>- 为特定用户或组共享，<br>- 专用于它所运行的线程，<br>- 只要客户端接口没有释放就一直存在，<br>- 或只要身份验证会话存在就一直存在</td>
</tr>
<tr>
<td>无状态</td>
<td>遵循标准的请求/响应模式</td>
</tr>
<tr>
<td>有状态</td>
<td>服务端实现可以与客户端接口同步，如通过websocket</td>
</tr>
<tr>
<td>双向</td>
<td>您可以定义回调，如使用WebSockets来进行即时通知</td>
</tr>
<tr>
<td>签名</td>
<td>在远程执行之前，要检查契约是否一致</td>
</tr>
<tr>
<td>安全</td>
<td>每个服务、方法都可以根据需要启用或禁用</td>
</tr>
<tr>
<td>安全</td>
<td>使用扩展的RESTful身份验证</td>
</tr>
<tr>
<td>多重托管<br>(DMZ)</td>
<td>默认情况下，服务托管在主ORM服务器中，但可以拥有自己的流程，并与ORM核心建立专用连接</td>
</tr>
<tr>
<td>代理可用</td>
<td>服务元数据可以由服务器选择性地显示</td>
</tr>
<tr>
<td>多种传输</td>
<td>mORMot的所有客户端-服务器协议都是可用的，即直接进程内连接、Windows消息、命名管道、TCP/IP-HTTP</td>
</tr>
<tr>
<td>基于JSON</td>
<td>传输的数据使用JavaScript对象表示法</td>
</tr>
<tr>
<td>路由选择</td>
<td>服务要么在URI级别（RESTful方式）标识，要么在JSON-RPC模型（AJAX方式）标识，要么通过任何自定义格式标识（使用类继承）标识</td>
</tr>
<tr>
<td>AJAX和RESTful</td>
<td>JSON和HTTP组合允许从AJAX富客户端使用服务</td>
</tr>
<tr>
<td>轻、快</td>
<td>性能和内存消耗非常优化，以确保可伸缩性和收益</td>
</tr>
</tbody>
</table>
<h2 id="toc_2">16.2. 如何提供服务<a class="vnote-anchor" href="#toc_2" data-anchor-icon="#"></a></h2>
<p>  要执行的典型基本任务如下：</p>
<ul>
<li>定义服务契约；</li>
<li>执行契约；</li>
<li>配置和托管服务；</li>
<li>构建客户端应用。</li>
</ul>
<p>  我们将说明这些条目。</p>
<h2 id="toc_3">16.3. 服务契约定义<a class="vnote-anchor" href="#toc_3" data-anchor-icon="#"></a></h2>
<p>  在SOA中，服务常常创建大量操作，为了便于实施和维护，应将操作归入公共服务。</p>
<p>  在说明此类服务是如何在mORMot中定义的之前，值得现说明<a href="">面向服务的体系结构（SOA）</a>主要原则，即松散耦合关系。定义mORMOt契约时，请确保此契约与其他契约解耦合。这将有助于编写SOLID代码，增强可维护性，并允许按需引入其他服务提供商（早晚有一天，您肯定会被要求用相应功能的第三方现有实现替换您的一项服务：您应该至少确保使用例如SOAP/WSDL网关以轻松地使用外部代码重新编码您自己的实现）。</p>
<h3 id="toc_4">16.3.1. 接口定义<a class="vnote-anchor" href="#toc_4" data-anchor-icon="#"></a></h3>
<p>  服务契约将被定义为普通的Delphi接口类型。实际上，与前面示例所述的类型一样，可以直接使用如下：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  ICalculator = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{9A60C8ED-CEB2-4E09-87D4-4A16F496E5FE}'</span>]
    <span class="hljs-comment">/// add two signed 32-bit integers</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Add</span><span class="hljs-params">(n1,n2: integer)</span>:</span> integer;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  这个<code>ICalculator.Add</code>方法将在<code>"ICalculator"</code>服务中定义一个<code>"Add“"</code>操作（根据惯例，它将在内部命名为<code>'Calculator'</code>）。此操作将期望两个数字作为输入，然后返回这些数字的和。</p>
<p>  目前的服务实现有以下要求：</p>
<ul>
<li>可以使用继承自<code>IInvokable</code>和<code>GUID</code>的任何接口，我们希望RTTI可用，因此<code>IInvokable</code>是一个很好的父类型；</li>
<li>您可以从现有类继承接口：在这种情况下，继承的方法将成为子接口的一部分，并且应该被实现（就像使用标准Delphi代码一样）；</li>
<li>类型定义只允许使用纯ASCII名称（因为通常使用英语拼写进行服务和操作命名）；</li>
<li>调用约定应该注册（Delphi的默认值），<code>stdcall</code>和<code>cdecl</code>都不可用，但这不是限制，因为接口定义专用于Delphi代码范围；</li>
<li>方法可以有结果，并接受每个值或每个引用的参数。</li>
</ul>
<p>  实际上，参数要求如下：</p>
<ul>
<li>处理简单类型（字符串，数字，日期，集合和枚举）和高级类型（对象，集合，记录和动态数组）；</li>
<li>它们可以定义为<code>const</code>，<code>var</code>或<code>out</code>，事实上，<code>const</code>和<code>var</code>参数值将作为JSON从客户端发送到服务器，<code>var</code>和<code>out</code>参数值将作为JSON从服务器返回；</li>
<li>允许<code>procedure</code>或<code>unction</code>的方法定义；</li>
<li>唯一的例外是函数不允许返回类实例（在这种情况下如何知道何时释放实例？），但这样的实例可以作为<code>const</code>，<code>var</code>或<code>out</code>参数传递（并且发布属性将被序列化在JSON消息中）；</li>
<li>实际上，框架没有直接处理<code>TCollection</code>类参数：你应该定义一个<code>TInterfacedCollection</code>类，覆盖它的<code>GetClass</code>抽象虚方法（否则服务器端无法按预期创建该集合）；</li>
<li>可以使用特殊<code>TServiceCustomAnswer</code>类型的记录作为函数结果来指定自定义内容（具有指定的编码，例如用于AJAX或HTML），在这种情况下，不应在方法中定义<code>var</code>也不输出参数值（仅返回<code>BLOB</code>值）。</li>
</ul>
<h3 id="toc_5">16.3.2. 服务方法参数<a class="vnote-anchor" href="#toc_5" data-anchor-icon="#"></a></h3>
<p>  处理的参数类型是：</p>
<table>
<thead>
<tr>
<th>Delphi类型</th>
<th>摘要</th>
</tr>
</thead>
<tbody>
<tr>
<td>BooleaIn</td>
<td>作为JSON true/false传输</td>
</tr>
<tr>
<td>integer cardinal<br>Int64 double currency<br>TDateTime TDateTimeMS</td>
<td>以JSON数字传输</td>
</tr>
<tr>
<td>enumerations</td>
<td>以JSON数字传输</td>
</tr>
<tr>
<td>set</td>
<td>作为JSON数字传输，每个元素1位（最多32个元素）</td>
</tr>
<tr>
<td>RawUTF8 WideString<br>SynUnicode</td>
<td>作为JSON文本传输（UTF-8编码）</td>
</tr>
<tr>
<td>string</td>
<td>作为UTF-8 JSON文本传输，但在Delphi 2009之前，该框架将确保客户端和服务器端使用相同的ANSI代码页——因此您应该在任何地方更好地使用RawUTF8</td>
</tr>
<tr>
<td>RawJSON</td>
<td>UTF-8缓冲区传输时不需要序列化(RawUTF8将转义为JSON字符串)——希望包含有效的JSON内容，例如TSQLTableJSON请求</td>
</tr>
<tr>
<td>RawByteString</td>
<td>以Base64编码的JSON文本传输——可选的二进制传输见下面</td>
</tr>
<tr>
<td>Persistent</td>
<td>发布的属性将作为JSON对象传输</td>
</tr>
<tr>
<td>TSQLRecord</td>
<td>所有发布的字段(包括ID)都将作为JSON对象传输</td>
</tr>
<tr>
<td>TCollection</td>
<td>不允许直接从TInterfacedCollection继承或调用TJSONSerializer.RegisterCollectionForJSON()</td>
</tr>
<tr>
<td>TInterfacedCollection</td>
<td>作为JSON对象的JSON数组传输</td>
</tr>
<tr>
<td>TObjectList</td>
<td>以JSON对象的JSON数组的形式传输，使用“ClassName”:“TMyClass”字段标识类型——请参阅TObjectList序列化</td>
</tr>
<tr>
<td>any TObject</td>
<td>参见TObject序列化</td>
</tr>
<tr>
<td>dynamic arrays</td>
<td>作为JSON数组传输——参见TDynArray动态数组包装器</td>
</tr>
<tr>
<td>record</td>
<td>需要有RTTI(一个字符串或动态数组字段)，就像对常规Delphi接口的期望一样——在Delphi 2010之前以二进制和Base64编码传输，或者由于增强的RTTI可用而以JSON对象传输，或者通过自定义JSON序列化——请参阅记录序列化</td>
</tr>
<tr>
<td>variant</td>
<td>以JSON格式传输，支持对象和数组的TDocVariant自定义变量类型;OLE变量数组不处理:使用_Arr([]) _ArrFast([])代替</td>
</tr>
<tr>
<td>TServiceCustomAnswer</td>
<td>如果用作函数结果(而不是参数)，则提供的内容将直接传输到客户机(不需要JSON序列化);在这种情况下，方法中不允许使用var或out参数——它将兼容我们的TServiceFactoryClient实现和任何其他服务使用者(例如AJAX)。</td>
</tr>
<tr>
<td>Interface</td>
<td>可以指定一个回调实例，以允许异步通知，例如使用websocket</td>
</tr>
</tbody>
</table>
<p>  因此，您可以定义复杂的接口类型，如下所示：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  ICalculator = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{9A60C8ED-CEB2-4E09-87D4-4A16F496E5FE}'</span>]
    <span class="hljs-comment">/// add two signed 32-bit integers</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Add</span><span class="hljs-params">(n1,n2: integer)</span>:</span> integer;
    <span class="hljs-comment">/// multiply two signed 64-bit integers</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Multiply</span><span class="hljs-params">(n1,n2: Int64)</span>:</span> Int64;
    <span class="hljs-comment">/// substract two floating-point values</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Subtract</span><span class="hljs-params">(n1,n2: double)</span>:</span> double;
    <span class="hljs-comment">/// convert a currency value into text</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ToText</span><span class="hljs-params">(Value: Currency; <span class="hljs-keyword">var</span> Result: RawUTF8)</span>;</span>
    <span class="hljs-comment">/// convert a floating-point value into text</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ToTextFunc</span><span class="hljs-params">(Value: double)</span>:</span> <span class="hljs-keyword">string</span>;
    <span class="hljs-comment">/// do some work with strings, sets and enumerates parameters,</span>
    <span class="hljs-comment">// testing also var (in/out) parameters and set as a function result</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SpecialCall</span><span class="hljs-params">(Txt: RawUTF8; <span class="hljs-keyword">var</span> Int: integer; <span class="hljs-keyword">var</span> Card: cardinal; field: TSynTableFieldTypes;
      fields: TSynTableFieldTypes; <span class="hljs-keyword">var</span> options: TSynTableFieldOptions)</span>:</span> TSynTableFieldTypes;
    <span class="hljs-comment">/// test integer, strings and wide strings dynamic arrays, together with records</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ComplexCall</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Ints: TIntegerDynArray; Strs1: TRawUTF8DynArray;
      <span class="hljs-keyword">var</span> Str2: TWideStringDynArray; <span class="hljs-keyword">const</span> Rec1: TVirtualTableModuleProperties;
      <span class="hljs-keyword">var</span> Rec2: TSQLRestCacheEntryValue)</span>:</span> TSQLRestCacheEntryValue;
    <span class="hljs-comment">/// test variant kind of parameters</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TestVariants</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Text: RawUTF8; V1: variant; <span class="hljs-keyword">var</span> V2: variant)</span>:</span> variant;
    <span class="hljs-comment">/// validates ArgsInputIsOctetStream raw binary upload</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DirectCall</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Data: TSQLRawBlob)</span>:</span> integer;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  注意<code>SpecialCall</code>和<code>ComplexCall</code>方法是如何定义相当复杂的参数，包括动态数组，集合和记录。 <code>DirectCall</code>将使用二进制POST，绕过Base64 JSON编码 。 框架将按预期处理<code>const</code>和<code>var</code>参数，即作为输入/输出参数，客户端也是如此。 任何简单类型的动态数组（如<code>TIntegerDynArray</code>，<code>TRawUTF8DynArray</code>或<code>TWideStringDynArray</code>）都将被序列化为普通的JSON数组，该框架能够处理任何动态数组定义，但会以更加AJAX兼容的方式序列化这些简单类型，这要归功于从Delphi 2010开始，增强了RTTI。</p>
<h3 id="toc_6">16.3.3. TPersistent/TSQLRecord参数<a class="vnote-anchor" href="#toc_6" data-anchor-icon="#"></a></h3>
<p>  如上所述，mORMot不允许方法函数返回类实例。</p>
<p>  也就是说，您无法定义这样的方法：</p>
<pre><code class="lang-pascal hljs">  ICustomerFactory = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{770D009F-15F4-4307-B2AD-BBAE42FE70C0}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">NewCustomer</span>:</span> TCustomer;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  在客户端-服务端模式，谁负责释放实例？ 对于这样的参数，在Delphi中没有标准的分配方案。 因此，每个<code>TObject</code>参数实例都应由调用者管理，即在调用之前分配并在之后释放。 该方法将只读取或写入实例的发布属性，并将它们序列化为JSON。</p>
<p>  您可以定义的是这样一种方法：</p>
<pre><code class="lang-pascal hljs">  ICustomerFactory = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{770D009F-15F4-4307-B2AD-BBAE42FE70C0}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">NewCustomer</span><span class="hljs-params">(<span class="hljs-keyword">out</span> aCustomer: TCustomer)</span>;</span>
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  注意，这里<code>out</code>关键字不表示如何分配内存，而是显示远程服务的通信方向，即它将在方法返回时序列化对象。 调用者应在调用之前实例化一个实例，而对于“普通”Delphi代码，由方法实例化实例并返回它。</p>
<p>  然后您的客户端代码可以这样使用它：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> Factory: ICustomerFactory;
    Repository: ICustomerRepository;
    Customer: TCustomer;
...
  Customer := TCustomer.Create; <span class="hljs-comment">// client side manage object instance</span>
  <span class="hljs-keyword">try</span>
    Customer.FirstName := StringToUTF8(EditFirstName.Text);
    Customer.LastName := StringToUTF8(EditLastName.Text);
    NewCutomerID := Repository.Save(Customer); <span class="hljs-comment">// persist the object</span>
  <span class="hljs-keyword">finally</span>
    Customer.Free; <span class="hljs-comment">// properly manage memory</span>
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  或者，同时使用Factory和Repository模式，如下所示：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> Factory: ICustomerFactory;
    Repository: ICustomerRepository;
    Customer: TCustomer;
...
  Factory.NewCustomer(Customer); <span class="hljs-comment">// get a new object instance</span>
  <span class="hljs-keyword">try</span>
    Customer.FirstName := StringToUTF8(EditFirstName.Text);
    Customer.LastName := StringToUTF8(EditLastName.Text);
    NewCutomerID := Repository.Save(Customer); <span class="hljs-comment">// persist the object</span>
  <span class="hljs-keyword">finally</span>
    Customer.Free; <span class="hljs-comment">// properly manage memory</span>
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  在现实中，可能很容易错误地编写一个返回现有实例的服务端方法，该实例将由服务器SOA调用者释放，并将在服务端随机触发意外的A/V ，非常难以跟踪。这是我们想要避免的...而指向nil的指针总是在客户端明确提示访问冲突，而不会影响服务端。</p>
<p>  因此，这个要求、限制设计使服务端对错误更具弹性，即使客户端使用起来有点复杂。通常，在客户端，您可以安全地预分配对象实例，并复用它们。</p>
<h3 id="toc_7">16.3.4. Record参数<a class="vnote-anchor" href="#toc_7" data-anchor-icon="#"></a></h3>
<p>  默认情况下，任何记录参数或函数结果将使用专有二进制（和优化）方案序列化，然后在Base64编码后作为JSON字符串传输。</p>
<p>  即使旧版本的Delphi无法为此类序列化生成所需的RTTI信息，只允许我们使用高效但专有的二进制布局，mORMot框架提供了一种实现任何自定义记录序列化的常用方法。请参阅<a href="">记录序列化</a>。</p>
<p>  请注意，用于记录的回调签名与用于动态数组序列化的回调签名相匹配，请参阅<a href="">动态数组序列化</a>，因为它将在两者之间共享。</p>
<p>  当记录在服务中用作数据传输对象时（这是常见的SOA实现模式中的一个好主意），这样的自定义序列化格式可以很方便，并且使用AJAX客户端使服务更加自然。</p>
<h3 id="toc_8">16.3.5. TCollection参数<a class="vnote-anchor" href="#toc_8" data-anchor-icon="#"></a></h3>
<h4 id="toc_9">16.3.5.1. 使用TCollection<a class="vnote-anchor" href="#toc_9" data-anchor-icon="#"></a></h4>
<p>  使用mORMot服务，您可以定义此类契约，例如<code>TCollTest</code>项目的<code>TCollTests</code>集合：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Collections</span><span class="hljs-params">(Item: TCollTest; <span class="hljs-keyword">var</span> List: TCollTests; <span class="hljs-keyword">out</span> Copy: TCollTests)</span>;</span>
</code></pre>
<p>  该契约的典型实施可能是：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TServiceComplexCalculator</span>.<span class="hljs-title">Collections</span><span class="hljs-params">(Item: TCollTest;
  <span class="hljs-keyword">var</span> List: TCollTests; <span class="hljs-keyword">out</span> Copy: TCollTests)</span>;</span>
<span class="hljs-keyword">begin</span>
  CopyObject(Item,List.Add);
  CopyObject(List,Copy);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  也就是说，它会将提供的<code>Item</code>对象添加到<code>List</code>内容中，然后在<code>Copy</code>内容中返回一个副本：</p>
<ul>
<li>没有<code>var</code>或<code>out</code>规范的设置项与<code>const</code>相同：它将从客户端序列化到服务器（而不是从服务器到服务器）;</li>
<li>将<code>List</code>设置为<code>var</code>参数将使该集合从客户端序列化到服务器，并从服务器返回到客户端;</li>
<li>将<code>Copy</code>用作<code>out</code>参数设置为只允许从服务器到客户端序列化此集合。</li>
</ul>
<p>  请注意，<code>const/var/out</code>类型的参数在契约级别使用，以指定序列化的方向，而不是像往常一样（即，定义它是通过值还是通过引用传递）。所有类参数都应在方法调用之前实例化：您不能将任何对象参数作为nil传递（也不能在函数结果中使用它）：它将引发错误。</p>
<p>  由于Delphi中TCollection类型的当前实现模式，不可能直接实现这种参数。</p>
<p>  实际上，TCollection构造函数定义如下：</p>
<pre><code class="lang-pascal hljs"> <span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">Create</span><span class="hljs-params">(ItemClass: TCollectionItemClass)</span>;</span>
</code></pre>
<p>  而且，在服务端，我们不知道要传递哪种<code>TCollectionItemClass</code>。 因此，<code>TServiceFactoryServer</code>无法正确实例化对象实例，从而提供预期的项类。</p>
<p>  该框架提出了两种可能的解决方</p>
<ul>
<li>您可以让您的集合类继承自新的<code>TInterfacedCollection</code>类型;</li>
<li>您可以调用<code>TJSONSerializer.RegisterCollectionForJSON()</code>方法来注册集合类型及其关联的项类。</li>
</ul>
<p>  我们现在将描述两种方式。</p>
<h4 id="toc_10">16.3.5.2. 继承自TInterfacedCollection<a class="vnote-anchor" href="#toc_10" data-anchor-icon="#"></a></h4>
<p>  已定义专用的<code>TInterfacedCollection</code>抽象类型：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-title">TInterfacedCollection</span> = <span class="hljs-keyword">class</span>(TCollection)
  <span class="hljs-keyword">protected</span>
    <span class="hljs-keyword">class</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetClass</span>:</span> TCollectionItemClass; <span class="hljs-keyword">virtual</span>; <span class="hljs-keyword">abstract</span>;
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">Create</span>;</span> <span class="hljs-keyword">reintroduce</span>; <span class="hljs-keyword">virtual</span>;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  为了使用对象集合，您必须至少定义抽象方法，例如：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-title">TCollTests</span> = <span class="hljs-keyword">class</span>(TInterfacedCollection)
  <span class="hljs-keyword">protected</span>
    <span class="hljs-keyword">class</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetClass</span>:</span> TCollectionItemClass; <span class="hljs-keyword">override</span>;
  <span class="hljs-keyword">end</span>;

<span class="hljs-keyword">class</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TCollTests</span>.<span class="hljs-title">GetClass</span>:</span> TCollectionItemClass;
<span class="hljs-keyword">begin</span>
  result := TCollTest;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  或者，如果您想要更完整/更方便的实现：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-title">TCollTests</span> = <span class="hljs-keyword">class</span>(TInterfacedCollection)
  <span class="hljs-keyword">private</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetCollItem</span><span class="hljs-params">(<span class="hljs-keyword">Index</span>: Integer)</span>:</span> TCollTest;
  <span class="hljs-keyword">protected</span>
    <span class="hljs-keyword">class</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetClass</span>:</span> TCollectionItemClass; <span class="hljs-keyword">override</span>;
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Add</span>:</span> TCollTest;
    <span class="hljs-keyword">property</span> Item[<span class="hljs-keyword">Index</span>: Integer]: TCollTest <span class="hljs-keyword">read</span> GetCollItem; <span class="hljs-keyword">default</span>;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  所有其他方法和属性（如<code>GetColItem/Add/Items[]</code>）将照常定义。</p>
<h4 id="toc_11">16.3.5.3. 注册TCollection类型<a class="vnote-anchor" href="#toc_11" data-anchor-icon="#"></a></h4>
<p>  使用TCollection类型参数的另一种方法是将其明确声明到框架中。 您应该使用相应的<code>TCollection/TCollectionItem</code>类类型对调用<code>JSONSerializer.RegisterCollectionForJSON()</code>。</p>
<p>  考虑一个专门的类：</p>
<pre><code class="lang-pascal hljs"> TMyCollection = <span class="hljs-keyword">type</span>(TCollection)
</code></pre>
<p>  请注意，此处需要专用类型。 你不能在普通的TCollection上使用这个注册。</p>
<p>  然后，例如，之后调用：</p>
<pre><code class="lang-pascal hljs">TJSONSerializer.RegisterCollectionForJSON(TMyCollection,TMyCollectionItem);
</code></pre>
<p>  以下代码行是相同的：</p>
<pre><code class="lang-pascal hljs"> MyColl := TMyCollection.Create(TMyCollectionItem);
 MyColl := ClassInstanceCreate(TMyCollection) <span class="hljs-keyword">as</span> TMyCollection;
 MyColl := ClassInstanceCreate(<span class="hljs-string">'TMyCollection'</span>) <span class="hljs-keyword">as</span> TMyCollection;
</code></pre>
<p>  最后两个将从先前的注册中检索关联的TMyCollectionItem类类型。</p>
<p>  由于这个内部注册表，mORMot将能够序列化和反序列化普通的TCollection类型。</p>
<h2 id="toc_12">16.4. 服务端<a class="vnote-anchor" href="#toc_12" data-anchor-icon="#"></a></h2>
<h3 id="toc_13">16.4.1. 服务契约实现<a class="vnote-anchor" href="#toc_13" data-anchor-icon="#"></a></h3>
<p>  为了获得操作服务，您需要实现与预期接口匹配的Delphi类。</p>
<p>  实际上，如前所述的示例类型，参见<a href="">接口</a>，可以直接使用：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TServiceCalculator</span> = <span class="hljs-keyword">class</span>(TInterfacedObject, ICalculator)
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Add</span><span class="hljs-params">(n1,n2: integer)</span>:</span> integer;
  <span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceCalculator</span>.<span class="hljs-title">Add</span><span class="hljs-params">(n1, n2: integer)</span>:</span> integer;
<span class="hljs-keyword">begin</span>
  result := n1+n2;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  这就是我们所需要的。 Delphi IDE将在编译时检查该类是否真正实现了指定的接口定义，因此将确保您的代码符合服务契约期望。 当初始化服务工厂时，框架将检查是否完全匹配（如处理参数类型），您将不会因为定义错误而遇到任何运行时异常。</p>
<p>  这里的类继承自<code>TInterfacedObject</code>，但你可以使用任何普通的Delphi类：唯一的条件是它实现了<code>ICalculator</code>接口。</p>
<h3 id="toc_14">16.4.2. 设置服务工厂<a class="vnote-anchor" href="#toc_14" data-anchor-icon="#"></a></h3>
<p>  为了获得工作服务，您需要初始化服务器端工厂，如下所示：</p>
<pre><code class="lang-pascal hljs"> Server.ServiceRegister(TServiceCalculator,[TypeInfo(ICalculator)],sicShared);
</code></pre>
<p>  如果先前已注册类型，您可能更喜欢直接使用没有<code>TypeInfo()</code>的接口类型：</p>
<pre><code class="lang-pascal hljs"> Server.ServiceDefine(TServiceCalculator,[ICalculator],sicShared);
</code></pre>
<p>  <code>Server</code>实例可以是任何<code>TSQLRestServer</code>继承类，实现了mORMot的客户端-服务端进程支持的任何协议，嵌入完整的SQLite3引擎（即<code>TSQLRestServerDB</code>类）或更轻的内存引擎（即<code>TSQLRestServerFullMemory</code>类 - 这就足够用于身份验证托管服务）。</p>
<p>  上面的代码行将注册<code>TServiceCalculator</code>类以实现<code>ICalculator</code>服务，具有单个共享实例生命周期（通过<code>sicShared</code>参数指定）。可以指定可选的超时值，以便在非活跃状态后自动释放已弃用的实例。</p>
<p>  无论何时执行服务，都可以使用实现类。通过指定<code>TServiceInstanceImplementation</code>值，在客户端和服务端定义此实现类的生命周期。客户端和服器端的此设置必须相同（将由框架检查）。</p>
<h3 id="toc_15">16.4.3. 实例的生命周期实现<a class="vnote-anchor" href="#toc_15" data-anchor-icon="#"></a></h3>
<p>  可用的实例管理选项如下：</p>
<table>
<thead>
<tr>
<th>生命周期</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>sicSingle</td>
<td>每次调用创建一个类实例:<br>- 这是实现服务最昂贵的方式，但对于简单的工作流(如一种类型的调用)是安全的;<br>- 这是<code>TSQLRestServer.ServiceRegister/ServiceDefine</code>方法的默认设置。</td>
</tr>
<tr>
<td>sicShared</td>
<td>一个对象实例用于所有调用输入，并且在调用之后不会被回收</td>
</tr>
<tr>
<td>sicClientDriven</td>
<td>将创建一个与客户端对应接口生命周期同步的对象实例，当客户端释放接口（超出作用域或设置为<code>nil</code>），它也会在服务端释放，在所有JSON请求中会转送一个数字标识符</td>
</tr>
<tr>
<td>sicPerSession</td>
<td>在整个运行会话期间将维护一个对象实例</td>
</tr>
<tr>
<td>sicPerUser</td>
<td>将维护一个对象实例并与运行中的用户关联</td>
</tr>
<tr>
<td>sicPerGroup</td>
<td>将维护一个对象实例，并与正在运行的用户的授权组关联</td>
</tr>
<tr>
<td>sicPerThread</td>
<td>将维护一个对象实例并与正在运行的线程关联</td>
</tr>
</tbody>
</table>
<p>  当然，<code>sicPerSession</code>、<code>sicPerUser</code>和<code>sicPerGroup</code>模式将期望对特定用户进行身份验证。 因此，只有在客户端和服务端之间启用RESTful身份验证时，这些实现模式才可用。</p>
<p>  每种模式的典型用法如下：</p>
<table>
<thead>
<tr>
<th>生命周期</th>
<th>用例</th>
</tr>
</thead>
<tbody>
<tr>
<td>sicSingle</td>
<td>异步进程(可能耗费资源)</td>
</tr>
<tr>
<td>sicShared</td>
<td>要么是一个非常简单的过程，要么需要一些全局数据</td>
</tr>
<tr>
<td>sicClientDriven</td>
<td>实现业务逻辑工作流的最佳候选</td>
</tr>
<tr>
<td>sicPerSession</td>
<td>维护特定于客户端应用程序的一些数据</td>
</tr>
<tr>
<td>sicPerUser</td>
<td>访问特定于某个用户的某些数据</td>
</tr>
<tr>
<td>sicPerGroup</td>
<td>访问某个用户类别（如管理员或来宾）共享的某些数据</td>
</tr>
<tr>
<td>sicPerThread</td>
<td>面向线程的过程（如正确初始化库）</td>
</tr>
</tbody>
</table>
<p>  在框架的当前实现中，类实例在内存中分配。</p>
<p>  这有两个后果：</p>
<ul>
<li>在客户端-服务器架构中，很可能会创建许多此类实例。因此，它必须不会大量消耗资源，特别是长生命周期中，您不应该在这些实例中存储任何<code>BLOB</code>，努力将内存使用限制到最小。对于更耗费资源的操作（需要内存和CPU资源的过程），首选<code>sicSingle</code>模式。</li>
<li>目前还没有内置数据持久性：服务实现应确保在调用期间保留内存数据（如未定义为<code>sicSingle</code>模式），即使服务器宕机时也不会丢失。由类来保持所需的数据，使用如<a href="">对象关系映射</a>。</li>
</ul>
<p>  还要注意，所有这些生命周期模式都希望方法实现代码在服务端是线程安全的并且是可重用的，只有<code>sicSingle</code>模式将拥有自己的运行实例，而<code>sicPerThread</code>的方法总是会运行在相同的线程上下文中。实际上，同一个用户可以打开多个连接，因此建议保护所有实现类方法进程，或者按预期设置执行选项 。</p>
<p>  为了说明<code>sicClientDriven</code>实现模式，让我们介绍以下接口及其实现（从框架提供的回归测试中提取）：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  IComplexNumber = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{29D753B2-E7EF-41B3-B7C3-827FEB082DC1}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Assign</span><span class="hljs-params">(aReal, aImaginary: double)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetImaginary</span>:</span> double;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetReal</span>:</span> double;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">SetImaginary</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: double)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">SetReal</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: double)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Add</span><span class="hljs-params">(aReal, aImaginary: double)</span>;</span>
    <span class="hljs-keyword">property</span> Real: double <span class="hljs-keyword">read</span> GetReal <span class="hljs-keyword">write</span> SetReal;
    <span class="hljs-keyword">property</span> Imaginary: double <span class="hljs-keyword">read</span> GetImaginary <span class="hljs-keyword">write</span> SetImaginary;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  此接口的目的是在其内部字段中存储复杂数字，然后检索它们的值，并定义<code>"Add"</code>方法，以执行添加操作。 我们使用属性和相关的<code>getter</code>和<code>setter</code>方法在代码中为<code>Real</code>和<code>Imaginary</code>字段上提供类似对象的行为。</p>
<p>  此接口在服务端由以下类实现：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TServiceComplexNumber</span> = <span class="hljs-keyword">class</span>(TInterfacedObject,IComplexNumber)
  <span class="hljs-keyword">private</span>
    fReal: double;
    fImaginary: double;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetImaginary</span>:</span> double;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetReal</span>:</span> double;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">SetImaginary</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: double)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">SetReal</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: double)</span>;</span>
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Assign</span><span class="hljs-params">(aReal, aImaginary: double)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Add</span><span class="hljs-params">(aReal, aImaginary: double)</span>;</span>
    <span class="hljs-keyword">property</span> Real: double <span class="hljs-keyword">read</span> GetReal <span class="hljs-keyword">write</span> SetReal;
    <span class="hljs-keyword">property</span> Imaginary: double <span class="hljs-keyword">read</span> GetImaginary <span class="hljs-keyword">write</span> SetImaginary;
  <span class="hljs-keyword">end</span>;

<span class="hljs-comment">{ TServiceComplexNumber }</span>

<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TServiceComplexNumber</span>.<span class="hljs-title">Add</span><span class="hljs-params">(aReal, aImaginary: double)</span>;</span>
<span class="hljs-keyword">begin</span>
  fReal := fReal+aReal;
  fImaginary := fImaginary+aImaginary;
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TServiceComplexNumber</span>.<span class="hljs-title">Assign</span><span class="hljs-params">(aReal, aImaginary: double)</span>;</span>
<span class="hljs-keyword">begin</span>
  fReal := aReal;
  fImaginary := aImaginary;
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceComplexNumber</span>.<span class="hljs-title">GetImaginary</span>:</span> double;
<span class="hljs-keyword">begin</span>
  result := fImaginary;
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceComplexNumber</span>.<span class="hljs-title">GetReal</span>:</span> double;
<span class="hljs-keyword">begin</span>
  result := fReal;
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TServiceComplexNumber</span>.<span class="hljs-title">SetImaginary</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: double)</span>;</span>
<span class="hljs-keyword">begin</span>
  fImaginary := Value;
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TServiceComplexNumber</span>.<span class="hljs-title">SetReal</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Value: double)</span>;</span>
<span class="hljs-keyword">begin</span>
  fReal := Value;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  此接口在服务端注册如下：</p>
<pre><code class="lang-pascal hljs"> Server.ServiceDefine(TServiceComplexNumber,[IComplexNumber],sicClientDriven);
</code></pre>
<p>  使用<code>sicClientDriven</code>模式，客户端也可以按需求处理自己的生命周期。 也就是说，只要需要，<code>fReal</code>和<code>fImaginary</code>字段都将保留在服务端。 由超时驱动的垃圾收集器将删除任何未关闭的挂起会话，因此即使在连接断开的情况下，也会释放在<code>sicClientDriven</code>模式下分配的资源。</p>
<h3 id="toc_16">16.4.4. 访问底层执行上下文<a class="vnote-anchor" href="#toc_16" data-anchor-icon="#"></a></h3>
<h4 id="toc_17">16.4.4.1. 从全局ServiceContext检索信息<a class="vnote-anchor" href="#toc_17" data-anchor-icon="#"></a></h4>
<p>  当执行任何基于接口的服务时，可以访问名为<code>ServiceContext</code>的全局<code>threadvar</code>来检索服务端当前运行的上下文。</p>
<p>  您可以访问以下信息，这些信息对于<code>sicPerSession</code>、<code>sicPerUser</code>和<code>sicPerGroup</code>实例生命周期模式非常有用：</p>
<pre><code class="lang-pascal hljs">  TServiceRunningContext = <span class="hljs-keyword">record</span>
    <span class="hljs-comment">/// the currently running service factory</span>
    <span class="hljs-comment">// - it can be used within server-side implementation to retrieve the</span>
    <span class="hljs-comment">// associated TSQLRestServer instance</span>
    <span class="hljs-comment">// - note that TServiceFactoryServer.Get() won't override this value, when</span>
    <span class="hljs-comment">// called within another service (i.e. if Factory is not nil)</span>
    Factory: TServiceFactoryServer;
    <span class="hljs-comment">/// the currently runnning context which launched the method</span>
    <span class="hljs-comment">// - low-level RESTful context is also available in its Call member</span>
    <span class="hljs-comment">// - Request.Server is the safe access point to the underlying TSQLRestServer,</span>
    <span class="hljs-comment">// unless the service is implemented via TInjectableObjectRest, so the</span>
    <span class="hljs-comment">// TInjectableObjectRest.Server property is preferred</span>
    <span class="hljs-comment">// - make available e.g. current session or authentication parameters</span>
    <span class="hljs-comment">// (including e.g. user details via Request.Server.SessionGetUser)</span>
    Request: TSQLRestServerURIContext;
    <span class="hljs-comment">/// the thread which launched the request</span>
    <span class="hljs-comment">// - is set by TSQLRestServer.BeginCurrentThread from multi-thread server</span>
    <span class="hljs-comment">// handlers - e.g. TSQLite3HttpServer or TSQLRestServerNamedPipeResponse</span>
    RunningThread: TThread;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  使用时，最好创建本地副本或<code>PServiceRunningContext</code>指针，因为访问<code>threadvar</code>具有无法忽略的性能成本。</p>
<p>  如果您的代码是在某些软件包中编译的，那么由于Delphi编译器/RTL限制（bug？），<code>threadvar</code>读将无法正常工作。 在这种情况下，您必须调用以下函数而不是直接访问<code>threadvar</code>：</p>
<pre><code class="lang-pascal hljs"> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CurrentServiceContext</span>:</span> TServiceRunningContext;
</code></pre>
<p>  请注意，在基于接口的服务方法调用超出作用域，此全局threadvar将重置为0。 因此从基于方法的服务中读取它将是无用的。</p>
<h4 id="toc_18">16.4.4.2. 通过<code>TInjectableObjectRest</code>实现服务<a class="vnote-anchor" href="#toc_18" data-anchor-icon="#"></a></h4>
<p>  <code>ServiceContext</code> threadvar的一个问题是，当在客户端/服务端上下文之外执行SOA方法时，不会填充执行上下文，如<code>TSQLRestServer</code>实例使用<code>Services.Resolve()</code>确实解决了它的依赖关系。</p>
<p>  更安全（且稍微更快）的替代方法是通过继承<code>TInjectableObjectRest</code>类来实现您的服务。</p>
<p>  这个类有自己的<code>Resolve()</code>重载方法（继承自<code>TInjectableObject</code>），还有两个额外的属性：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-title">TInjectableObjectRest</span> = <span class="hljs-keyword">class</span>(TInjectableObject)
  ...
  <span class="hljs-keyword">public</span>
    <span class="hljs-keyword">property</span> Factory: TServiceFactoryServer <span class="hljs-keyword">read</span> fFactory;
    <span class="hljs-keyword">property</span> Server: TSQLRestServer <span class="hljs-keyword">read</span> fServer;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  这些属性将由<code>TServiceFactoryServer.CreateInstance</code>注入，如服务实现的对象在服务端实例化时。它们将提供对底层REST服务器的直接和安全访问，如它的所有ORM方法。</p>
<h3 id="toc_19">16.4.5. 使用服务端服务<a class="vnote-anchor" href="#toc_19" data-anchor-icon="#"></a></h3>
<p>  一旦服务在服务端注册，就很容易在代码中使用它。</p>
<p>  在复杂的面向服务的架构（SOA）中，让服务相互调用并不是一种好的做法。代码解耦是可维护性的关键。但在某些情况下，您将不得不在服务端使用服务，特别是如果您的软件是多层架构（如领域驱动设计），您的应用程序服务可以解耦，但领域驱动的服务（业务模型实现）可以在另一个客户端-服务器层级上，使用专用协议，并且可以嵌套调用。</p>
<p>  在这种情况下，根据SOLID设计原则，您最好在代码中依赖抽象，即不调用服务实现（如<code>TInterfacedObject</code>实例，更糟糕的是直接调用底层类或函数），而是服务抽象接口。您可以使用<code>TSQLRest.Services</code>实例的以下方法（请注意，此方法在客户端和服务端都可使用其抽象的<code>TServiceFactory</code>，因此是所有服务的正确访问点）：</p>
<pre><code class="lang-pascal hljs"> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceFactory</span>.<span class="hljs-title">Get</span><span class="hljs-params">(<span class="hljs-keyword">out</span> Obj)</span>:</span> Boolean;
</code></pre>
<p>  您有几种方法可以从服务名称、GUID或列表索引检索<code>TServiceFactory</code>实例。</p>
<p>  也就是说，您可以编码：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> I: ICalculator;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> ServiceContext.Request.Server.Services[<span class="hljs-string">'Calculator'</span>].Get(I)) <span class="hljs-keyword">then</span>
    result := I.Add(<span class="hljs-number">10</span>,<span class="hljs-number">20</span>);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  或者，对于更复杂的服务：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> CN: IComplexNumber;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> ServiceContext.Request.Server.Services.Resolve(IComplexNumber,CN) <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">exit</span>; <span class="hljs-comment">// IComplexNumber interface not found</span>
  CN.Real := <span class="hljs-number">0.01</span>;
  CN.Imaginary := <span class="hljs-number">3.1415</span>;
  CN.Add(<span class="hljs-number">100</span>,<span class="hljs-number">200</span>);
  assert(SameValue(CN.Real,<span class="hljs-number">100.01</span>));
  assert(SameValue(CN.Imaginary,<span class="hljs-number">203.1415</span>));
<span class="hljs-keyword">end</span>; <span class="hljs-comment">// here CN will be released</span>
</code></pre>
<p>  对于更新的Delphi泛型版本（即Delphi 2010及以上版本，因为Delphi 2009对泛型有bug），您可以使用这样的方法，它支持编译时检查：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> I: ICalculator;
<span class="hljs-keyword">begin</span>
  I := Server.Service&lt;ICalculator&gt;;
  <span class="hljs-keyword">if</span> I&lt;&gt;<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span>
    result := I.Add(<span class="hljs-number">10</span>,<span class="hljs-number">20</span>);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  如果您愿意，您可以使用本地字段中缓存、存储<code>TServiceFactory</code>或<code>TSQLRest</code>实例。 使用<code>ServiceContext.Request.Server</code>繁琐且容易出错。</p>
<p>  但您可以考虑替换成<a href="">通过TInjectableObjectRest实现服务</a>，<code>TInjectableObjectRest</code>类已经内置了<code>Resolve()</code>重载方法，并且可以直接访问底层<code>Server: TSQLRestServer</code>实例。 因此，您能够直接编写SOA和ORM代码：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> I: ICalculator;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> Resolve(ICalculator,I) <span class="hljs-keyword">then</span>
    Server.Add(TSQLRecordExecution,[<span class="hljs-string">'Add'</span>,I.Add(<span class="hljs-number">10</span>,<span class="hljs-number">20</span>)]);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  如果服务被定义为<code>sicPerThread</code>，那么您在服务端检索的实例将特定于正在运行的线程，在这种情况下，缓存实例可能会引起混淆，因为每个线程将有一个专用实例。</p>
<h2 id="toc_20">16.5. 客户端<a class="vnote-anchor" href="#toc_20" data-anchor-icon="#"></a></h2>
<p>  客户端根本没有实现，这是mORMot服务的神奇之处：无需调用向导（如DataSnap、RemObjects或WCF），也无需编写客户端方法，与我们的<a href="">基于方法的客户端-服务端服务</a>一样。</p>
<p>  您只需注册现有的接口定义（例如我们的<code>ICalculator</code>类型），您就可以通过远程访问在服务器端执行所有方法。</p>
<p>  实际上，框架将创建一个隐藏的“假”<code>TInterfaceObject</code>类（包括其内部VTable和低级汇编程序代码），并用于与远程服务器交互。但是您不必担心这个过程，它对您的代码是透明的。</p>
<h3 id="toc_21">16.5.1. 设置客户端工厂<a class="vnote-anchor" href="#toc_21" data-anchor-icon="#"></a></h3>
<p>  在客户端，您必须注册相应的接口以初始化其关联的工厂，如下所示：</p>
<pre><code class="lang-pascal hljs">Client.ServiceRegister([TypeInfo(ICalculator)],sicShared);
</code></pre>
<p>  如果先前已注册类型，您可能更喜欢直接使用没有<code>TypeInfo()</code>的接口类型：</p>
<pre><code class="lang-pascal hljs"> Client.ServiceDefine([ICalculator],sicShared);
</code></pre>
<p>  它服务端注册非常相似，我们在这里没有提供任何实现类，因为实现保留在服务端。</p>
<p>  请注意，实现模式（此处为<code>sicShared</code>）应与服务端使用的模式匹配。 如果此设置不一致，则会发生错误。</p>
<p>  我们之前谈到的另一个接口，即<code>IComplexNumber</code>，在客户端使用如下代码注册：</p>
<pre><code class="lang-pascal hljs">Client.ServiceDefine([IComplexNumber],sicClientDriven);
</code></pre>
<p>  这将创建相应的<code>TServiceFactoryClient</code>实例，并为客户端进程提供“虚假”实现类。</p>
<p>  更准确地说，这个注册步骤在客户端确实不是强制性的。 如果使用<code>TServiceContainerClient.Info()</code>方法，则客户端实现将在<code>sicClientDriven</code>实现模式下自动注册提供的接口。</p>
<h3 id="toc_22">16.5.2. 在客户端使用服务<a class="vnote-anchor" href="#toc_22" data-anchor-icon="#"></a></h3>
<p>  一旦在客户端注册了服务，就可以很容易地在代码中使用它。</p>
<p>  您可以使用与服务端相同的方法来检索<code>TServiceFactory</code>实例。</p>
<p>  也就是说，您可以编码：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> I: ICalculator;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> Client.Services[<span class="hljs-string">'Calculator'</span>].Get(I)) <span class="hljs-keyword">then</span>
    result := I.Add(<span class="hljs-number">10</span>,<span class="hljs-number">20</span>);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  对于Delphi 2010及更高版本，您可以使用基于泛型的方法，该方法支持编译时检查：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> I: ICalculator;
<span class="hljs-keyword">begin</span>
  I := Client.Service&lt;ICalculator&gt;;
  <span class="hljs-keyword">if</span> I&lt;&gt;<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span>
    result := I.Add(<span class="hljs-number">10</span>,<span class="hljs-number">20</span>);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  对于更复杂的服务，初始化为<code>sicClientDriven</code>：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> CN: IComplexNumber;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> Client.Services.Resolve(IComplexNumber,CN) <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">exit</span>; <span class="hljs-comment">// IComplexNumber interface not found</span>
  CN.Real := <span class="hljs-number">0.01</span>;
  CN.Imaginary := <span class="hljs-number">3.1415</span>;
  CN.Add(<span class="hljs-number">100</span>,<span class="hljs-number">200</span>);
  assert(SameValue(CN.Real,<span class="hljs-number">100.01</span>));
  assert(SameValue(CN.Imaginary,<span class="hljs-number">203.1415</span>));
<span class="hljs-keyword">end</span>; <span class="hljs-comment">// here CN will be released on both client AND SERVER sides</span>
</code></pre>
<p>  代码与服务器上的代码相同。唯一的功能变化是执行将在服务器端进行（使用已注册的<code>TServiceComplexNumber</code>实现类），并且相应的类实例将保持活动状态，直到在客户端上释放<code>CN</code>本地接口。</p>
<p>  如果您愿意，您可以在本地字段中缓存<code>TServiceFactory</code>实例。在客户端，即使服务已被定义为<code>sicPerThread</code>，您也可以安全地缓存并重用相同的实例，因为每个线程进程仅在服务端进行。</p>
<p>  正如我们在前一段中所述，由于<code>IComplexNumber</code>将作为<code>sicClientDriven</code>执行，因此不必为此接口调用<code>Client.ServiceRegister</code>或<code>ServiceDefine</code>方法。实际上，在<code>Client.Services.Info(TypeInfo(IComplexNumber))</code>方法执行期间，如果之前没有明确地注册，则将自动注册。为了代码可读性，在客户端显式注册接口也是一个好主意，只是为了强调该接口即将被使用，以及在哪种模式下使用。</p>
<h2 id="toc_23">16.6. 示例代码<a class="vnote-anchor" href="#toc_23" data-anchor-icon="#"></a></h2>
<p>  您可以在提供的源代码分发的<code>"SQLite3/Samples/14 - Interface based services"</code>文件夹中找到有关此功能的专用示例。</p>
<p>  此代码的目的是展示如何使用接口通过命名管道通信创建客户端-服务端服务。</p>
<h3 id="toc_24">16.6.1. 共享契约<a class="vnote-anchor" href="#toc_24" data-anchor-icon="#"></a></h3>
<p>  首先，您将找到一个由客户端和服务端应用共享的公共单元：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">unit</span> Project14Interface;

<span class="hljs-keyword">interface</span>

<span class="hljs-keyword">type</span>
  ICalculator = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{9A60C8ED-CEB2-4E09-87D4-4A16F496E5FE}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Add</span><span class="hljs-params">(n1,n2: integer)</span>:</span> integer;
  <span class="hljs-keyword">end</span>;

<span class="hljs-keyword">const</span>
  ROOT_NAME = <span class="hljs-string">'service'</span>;
  PORT_NAME = <span class="hljs-string">'888'</span>;
  APPLICATION_NAME = <span class="hljs-string">'RestService'</span>;

<span class="hljs-keyword">implementation</span>

<span class="hljs-keyword">uses</span> mORMot;

<span class="hljs-keyword">initialization</span>
  TInterfaceFactory.RegisterInterfaces([TypeInfo(ICalculator)]);
<span class="hljs-keyword">end</span>.
</code></pre>
<p>  此单元的唯一用途是定义服务接口，<code>ROOT_NAME</code>用于ORM模型（用于RESTful URI方案），<code>APPLICATION_NAME</code>用于命名管道通信。</p>
<p>  此<code>ICalculator</code>类型也为内部接口工厂系统注册，因此您可以直接使用<code>ICalculator</code>而不是<code>TypeInfo(ICalculator)</code>来使用框架方法。</p>
<h3 id="toc_25">16.6.2. 服务端应用示例<a class="vnote-anchor" href="#toc_25" data-anchor-icon="#"></a></h3>
<p>  服务器实现如下：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">program</span> Project14Server;

<span class="hljs-meta">{$APPTYPE CONSOLE}</span>

<span class="hljs-keyword">uses</span>
  SysUtils,
  mORMot,
  mORMotSQLite3,
  Project14Interface;

<span class="hljs-keyword">type</span>
  <span class="hljs-title">TServiceCalculator</span> = <span class="hljs-keyword">class</span>(TInterfacedObject, ICalculator)
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Add</span><span class="hljs-params">(n1,n2: integer)</span>:</span> integer;
  <span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceCalculator</span>.<span class="hljs-title">Add</span><span class="hljs-params">(n1, n2: integer)</span>:</span> integer;
<span class="hljs-keyword">begin</span>
  result := n1+n2;
<span class="hljs-keyword">end</span>;

<span class="hljs-keyword">var</span>
  aModel: TSQLModel;
<span class="hljs-keyword">begin</span>
  aModel := TSQLModel.Create([],ROOT_NAME);
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">with</span> TSQLRestServerDB.Create(aModel,ChangeFileExt(paramstr(<span class="hljs-number">0</span>),<span class="hljs-string">'.db'</span>),true) <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">try</span>
      CreateMissingTables; <span class="hljs-comment">// we need AuthGroup and AuthUser tables</span>
      ServiceDefine(TServiceCalculator,[ICalculator],sicShared);
      <span class="hljs-keyword">if</span> ExportServerNamedPipe(APPLICATION_NAME) <span class="hljs-keyword">then</span>
        writeln(<span class="hljs-string">'Background server is running.'</span><span class="hljs-string">#10</span>) <span class="hljs-keyword">else</span>
        writeln(<span class="hljs-string">'Error launching the server'</span><span class="hljs-string">#10</span>);
      <span class="hljs-keyword">write</span>(<span class="hljs-string">'Press [Enter] to close the server.'</span>);
      readln;
    <span class="hljs-keyword">finally</span>
      Free;
    <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">finally</span>
    aModel.Free;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>.
</code></pre>
<p>  它将实例化一个包含SQLite3数据库引擎的<code>TSQLRestServerDB</code>类。 事实上，由于我们需要身份验证，因此<code>AuthGroup</code>和<code>AuthUser</code>表都需要使用。</p>
<p>  然后调用<code>ServiceDefine()</code>将定义<code>ICalculator</code>契约，并将<code>TServiceCalculator</code>类用作其实现。 使用<code>sicShared</code>模式，因为在所有调用期间可以共享相同的实现类（没有共享或私有数据需要管理）。</p>
<p>  请注意，由于此服务器的数据库只需要基本的（只需要CRUD命令来处理身份验证表），我们使用<code>TSQLRestServerFullMemory</code>类而不是<code>TSQLRestServerDB</code>。 这就是Project14ServerInMemory.dpr示例的目的：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">program</span> Project14ServerInMemory;
  (...)
    <span class="hljs-keyword">with</span> TSQLRestServerFullMemory.Create(aModel,<span class="hljs-string">'test.json'</span>,false,true) <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">try</span>
      ServiceDefine(TServiceCalculator,[ICalculator],sicShared);
      <span class="hljs-keyword">if</span> ExportServerNamedPipe(APPLICATION_NAME) <span class="hljs-keyword">then</span>
  (...)
</code></pre>
<p>  使用该类将包括<code>CreateMissingTables</code>调用，以创建身份验证所需的<code>AuthGroup</code>和<code>AuthUser</code>表。 但是生成的可执行文件会更轻：使用Delphi 7和我们的LVCL类编译时，只有200 KB，提供完整的服务。</p>
<h3 id="toc_26">16.6.3. 客户端应用示例<a class="vnote-anchor" href="#toc_26" data-anchor-icon="#"></a></h3>
<p>  客户端只是一个带有两个<code>TEdit</code>字段（<code>edtA</code>和<code>edtB</code>）的简单表单，以及一个"Call" 按钮，其中<code>OnClick</code>事件实现为：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TForm1</span>.<span class="hljs-title">btnCallClick</span><span class="hljs-params">(Sender: TObject)</span>;</span>
<span class="hljs-keyword">var</span> a,b: integer;
    err: integer;
    I: ICalculator;
<span class="hljs-keyword">begin</span>
  val(edtA.Text,a,err);
  <span class="hljs-keyword">if</span> err&lt;&gt;<span class="hljs-number">0</span> <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
    edtA.SetFocus;
    <span class="hljs-keyword">exit</span>;
  <span class="hljs-keyword">end</span>;
  val(edtB.Text,b,err);
  <span class="hljs-keyword">if</span> err&lt;&gt;<span class="hljs-number">0</span> <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
    edtB.SetFocus;
    <span class="hljs-keyword">exit</span>;
  <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">if</span> Client=<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
    <span class="hljs-keyword">if</span> Model=<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span>
      Model := TSQLModel.Create([],ROOT_NAME);
    Client := TSQLRestClientURINamedPipe.Create(Model,APPLICATION_NAME);
    Client.SetUser(<span class="hljs-string">'User'</span>,<span class="hljs-string">'synopse'</span>);
    Client.ServiceDefine([ICalculator],sicShared);
  <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">if</span> Client.Services[<span class="hljs-string">'Calculator'</span>].Get(I) <span class="hljs-keyword">then</span>
    lblResult.Caption := IntToStr(I.Add(a,b));
<span class="hljs-keyword">end</span>; <span class="hljs-comment">// here local I will be released</span>
</code></pre>
<p>  客户端代码初始化为：</p>
<ul>
<li>创建<code>TSQLRestClientURINamedPipe</code>实例，关联<code>TSQLModel</code>和给定的<code>APPLICATION_NAME</code>通过命名管道通信访问正确的服务器；</li>
<li>使用默认的“用户”权限对连接进行身份验证；</li>
<li><code>ICalculator</code>接口在客户端的内部工厂中以<code>sicShared</code>模式定义（就像在服务器中一样）。</li>
</ul>
<p>  一旦客户端启动并准备就绪，就会检索<code>I: ICalculator</code>局部变量实例，并通过简单的<code>I.Add(a,b)</code>语句直接调用远程服务。</p>
<p>  您可以想象使用mORMot为未来的应用程序实现面向服务的体系结构（SOA）是多么容易和安全。</p>
<h3 id="toc_27">16.6.4. 增强示例：远程SQL访问<a class="vnote-anchor" href="#toc_27" data-anchor-icon="#"></a></h3>
<p>  您将在<code>SQLite3\Samples\16 - Execute SQL via services</code>找到通过JSON和HTTP访问任何外部数据库的客户端-服务端示例。它很好地演示了如何在客户端和服务器之间使用基于接口的服务。它还将展示我们的<code>SynDB.pas</code>类如何进行非常抽象的设计，并且易于使用，无论您需要使用何种数据库驱动程序。</p>
<p>  已定义相应的服务契约：</p>
<pre><code class="lang-pascal hljs">  TRemoteSQLEngine = (rseOleDB, rseODBC, rseOracle, rseSQlite3, rseJet, rseMSSQL);

  IRemoteSQL = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{9A60C8ED-CEB2-4E09-87D4-4A16F496E5FE}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Connect</span><span class="hljs-params">(aEngine: TRemoteSQLEngine; <span class="hljs-keyword">const</span> aServerName, aDatabaseName,
      aUserID, aPassWord: RawUTF8)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetTableNames</span>:</span> TRawUTF8DynArray;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Execute</span><span class="hljs-params">(<span class="hljs-keyword">const</span> aSQL: RawUTF8; aExpectResults, aExpanded: Boolean)</span>:</span> RawJSON;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  这项服务的目的是：</p>
<ul>
<li><code>Connect()</code>到任何外部数据库，使用需要的参数调用标准<code>TSQLDBConnectionProperties.Create()</code>构造函数；</li>
<li>以列表的形式检索此外部数据库的所有表名称；</li>
<li>执行任何SQL语句，将内容作为JSON数组返回，准备好由AJAX应用程序（如果<code>aExpanded</code>为<code>true</code>）或Delphi客户端（如通过<code>TSQLTableJSON</code>和<code>mORMotUI</code>单元）使用。</li>
</ul>
<p>  当然，此服务将在<code>sicClientDriven</code>模式下定义。也就是说，该框架将能够管理客户端驱动的<code>TSQLDBProperties</code>实例生命周期。</p>
<p>  这项服务的好处是客户端不需要数据库连接：常规HTTP连接就足够了，无需安装或配置任何数据库驱动程序。</p>
<p>  由于mORMot优化的JSON序列化，使用这种普通的HTTP/JSON服务可能会更快，而不是通过VPN进行数据库连接。事实上，数据库连接可以在本地网络上运行，并且不使用常见的Internet上的高延迟连接。且mORMot客户端-服务器进程针对此类连接进行了优化。</p>
<p>  请注意，<code>Execute()</code>方法返回<code>RawJSON</code>类型的变量，实际上是<code>RawUTF8</code>的子类型。其目的是直接传输UTF-8编码的内容，而不转换为JSON字符串，就像RawUTF8变量的情况一样。实际上，在JSON字符串中转义一些JSON数组非常冗长。在这种情况下使用RawJSON可确保最佳的客户端和服务器端速度，并降低传输带宽。</p>
<p>  服务器部分很容易遵循：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TServiceRemoteSQL</span> = <span class="hljs-keyword">class</span>(TInterfacedObject, IRemoteSQL)
  <span class="hljs-keyword">protected</span>
    fProps: TSQLDBConnectionProperties;
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">destructor</span> <span class="hljs-title">Destroy</span>;</span> <span class="hljs-keyword">override</span>;
  <span class="hljs-keyword">public</span> <span class="hljs-comment">// implements IRemoteSQL methods</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Connect</span><span class="hljs-params">(aEngine: TRemoteSQLEngine; <span class="hljs-keyword">const</span> aServerName, aDatabaseName,
      aUserID, aPassWord: RawUTF8)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetTableNames</span>:</span> TRawUTF8DynArray;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Execute</span><span class="hljs-params">(<span class="hljs-keyword">const</span> aSQL: RawUTF8; aExpectResults, aExpanded: Boolean)</span>:</span> RawJSON;
  <span class="hljs-keyword">end</span>;

<span class="hljs-comment">{ TServiceRemoteSQL }</span>

<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TServiceRemoteSQL</span>.<span class="hljs-title">Connect</span><span class="hljs-params">(aEngine: TRemoteSQLEngine;
  <span class="hljs-keyword">const</span> aServerName, aDatabaseName, aUserID, aPassWord: RawUTF8)</span>;</span>
<span class="hljs-keyword">const</span> <span class="hljs-comment">// rseOleDB, rseODBC, rseOracle, rseSQlite3, rseJet, rseMSSQL</span>
  TYPES: <span class="hljs-keyword">array</span>[TRemoteSQLEngine] <span class="hljs-keyword">of</span> TSQLDBConnectionPropertiesClass = (
     TOleDBConnectionProperties, TODBCConnectionProperties,
     TSQLDBOracleConnectionProperties, TSQLDBSQLite3ConnectionProperties,
     TOleDBJetConnectionProperties, TOleDBMSSQL2008ConnectionProperties);
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> fProps&lt;&gt;<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">raise</span> Exception.Create(<span class="hljs-string">'Connect called more than once'</span>);
  fProps := TYPES[aEngine].Create(aServerName,aDatabaseName,aUserID,aPassWord);
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceRemoteSQL</span>.<span class="hljs-title">Execute</span><span class="hljs-params">(<span class="hljs-keyword">const</span> aSQL: RawUTF8; aExpectResults, aExpanded: Boolean)</span>:</span> RawJSON;
<span class="hljs-keyword">var</span> res: ISQLDBRows;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> fProps=<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">raise</span> Exception.Create(<span class="hljs-string">'Connect call required before Execute'</span>);
  res := fProps.ExecuteInlined(aSQL,aExpectResults);
  <span class="hljs-keyword">if</span> res=<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span>
    result := <span class="hljs-string">''</span> <span class="hljs-keyword">else</span>
    result := res.FetchAllAsJSON(aExpanded);
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceRemoteSQL</span>.<span class="hljs-title">GetTableNames</span>:</span> TRawUTF8DynArray;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> fProps=<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">raise</span> Exception.Create(<span class="hljs-string">'Connect call required before GetTableNames'</span>);
  fProps.GetTableNames(result);
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">destructor</span> <span class="hljs-title">TServiceRemoteSQL</span>.<span class="hljs-title">Destroy</span>;</span>
<span class="hljs-keyword">begin</span>
  FreeAndNil(fProps);
  <span class="hljs-keyword">inherited</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  <code>SynDB.pas</code>过程中的任何异常，或者在用例错误的情况下手动引发的异常都将被传输到客户端，正如预期的那样。 <code>fProps</code>实例的生命周期由客户端处理，因此我们只需要在服务实现析构函数中释放它的指针。</p>
<p>  使用以下代码在服务器端初始化服务：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span>
  aModel: TSQLModel;
  aServer: TSQLRestServer;
  aHTTPServer: TSQLHttpServer;
<span class="hljs-keyword">begin</span>
  <span class="hljs-comment">// define the log level</span>
  <span class="hljs-keyword">with</span> TSQLLog.Family <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
    Level := LOG_VERBOSE;
    EchoToConsole := LOG_VERBOSE; <span class="hljs-comment">// log all events to the console</span>
  <span class="hljs-keyword">end</span>;
  <span class="hljs-comment">// manual switch to console mode</span>
  AllocConsole;
  TextColor(ccLightGray);
  <span class="hljs-comment">// create a Data Model</span>
  aModel := TSQLModel.Create([],ROOT_NAME);
  <span class="hljs-keyword">try</span>
    <span class="hljs-comment">// initialize a TObjectList-based database engine</span>
    aServer := TSQLRestServerFullMemory.Create(aModel,<span class="hljs-string">'users.json'</span>,false,true);
    <span class="hljs-keyword">try</span>
      <span class="hljs-comment">// register our IRemoteSQL service on the server side</span>
      aServer.ServiceRegister(TServiceRemoteSQL,[TypeInfo(IRemoteSQL)],sicClientDriven).
        <span class="hljs-comment">// fProps should better be executed and released in the one main thread</span>
        SetOptions([],[optExecInMainThread,optFreeInMainThread]);
      <span class="hljs-comment">// launch the HTTP server</span>
      aHTTPServer := TSQLHttpServer.Create(<span class="hljs-string">'888'</span>,[aServer],<span class="hljs-string">'+'</span>,useHttpApiRegisteringURI);
      <span class="hljs-keyword">try</span>
        aHTTPServer.AccessControlAllowOrigin := <span class="hljs-string">'*'</span>; <span class="hljs-comment">// for AJAX requests to work</span>
        writeln(<span class="hljs-string">#10</span><span class="hljs-string">'Background server is running.'</span><span class="hljs-string">#10</span>);
        writeln(<span class="hljs-string">'Press [Enter] to close the server.'</span><span class="hljs-string">#10</span>);
        ConsoleWaitForEnterKey;
      <span class="hljs-keyword">finally</span>
        aHTTPServer.Free;
      <span class="hljs-keyword">end</span>;
    <span class="hljs-keyword">finally</span>
      aServer.Free;
    <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">finally</span>
    aModel.Free;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>.
</code></pre>
<p>  这是典型的mORMot服务端初始化，通过HTTP通信协议发布（如果可能，具有自动注册功能，如<code>useHttpApiRegisteringURI</code>标志所述）。 由于我们不会将ORM用于任何目的而仅是使用身份验证，因此基于<code>TObjectList</code>的快速引擎（即<code>TSQLRestServerFullMemory</code>）足以满足此示例目的。</p>
<p>  在上面的代码中，您可以注意到<code>IRemoteSQL</code>服务是使用<code>optExecInMainThread</code>和<code>optFreeInMainThread</code>选项定义的。 这意味着所有方法都将在主进程线程中执行。 实际上，由于<code>SynDB.pas</code>数据库访问可能会为每个线程打开一个连接（例如，对于OleDB / MS SQL或Oracle提供程序），它可能会占用大量内存。 强制在主线程数据库中执行可降低资源消耗，并且仍将保持合适的速度执行（因为所有内部编码和通信将在框架单元中是多线程的）。</p>
<p>  从客户的角度来看，将这样使用：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TMainForm</span>.<span class="hljs-title">FormShow</span><span class="hljs-params">(Sender: TObject)</span>;</span>
  (...)
  fModel := TSQLModel.Create([],ROOT_NAME);
  fClient := TSQLHttpClient.Create(<span class="hljs-string">'localhost'</span>,<span class="hljs-string">'888'</span>,fModel);
  <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> fClient.ServerTimestampSynchronize <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
    ShowLastClientError(fClient,<span class="hljs-string">'Please run Project16ServerHttp.exe'</span>);
    Close;
    <span class="hljs-keyword">exit</span>;
  <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">if</span> (<span class="hljs-keyword">not</span> fClient.SetUser(<span class="hljs-string">'User'</span>,<span class="hljs-string">'synopse'</span>)) <span class="hljs-keyword">or</span>
     (<span class="hljs-keyword">not</span> fClient.ServiceRegisterClientDriven(TypeInfo(IRemoteSQL),fService)) <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
    ShowLastClientError(fClient,<span class="hljs-string">'Remote service not available on server'</span>);
    Close;
    <span class="hljs-keyword">exit</span>;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  我们的<code>IRemoteSQL</code>服务将在<code>sicClientDriven</code>模式下访问，因此我们需要初始化<code>RESTful</code>身份验证，正确调用<code>SetUser()</code>。</p>
<p>  注意使用<code>mORMotUILogin</code>单元的<code>ShowLastClientError()</code>函数，能够使用我们的<code>SynTaskDialog</code>单元报告有关最新错误的标准和详细信息。</p>
<p>  在此示例中，ORM模型未定义任何表。 没有必要，因为所有外部进程都将在SQL级别进行。 由于我们需要身份验证（请参阅<code>fClient.SetUser</code>方法调用），ORM核心本身会将<code>TSQLAuthUser</code>和<code>TSQLAuthGroup</code>表添加到模型中，无需显式添加它们。</p>
<p>  从现在开始，我们有一个<code>fService: IRemoteSQL</code>实例可用于连接和处理任何远程SQL请求。</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TMainForm</span>.<span class="hljs-title">btnOpenClick</span><span class="hljs-params">(Sender: TObject)</span>;</span>
<span class="hljs-keyword">var</span> TableNames: TRawUTF8DynArray;
  (...)
  <span class="hljs-keyword">with</span> fSettings <span class="hljs-keyword">do</span>
    fService.Connect(Engine,ServerName,DatabaseName,UserID,PassWord);
  TableNames := fService.GetTableNames;
  cbbTableNames.Items.Text := UTF8ToString(RawUTF8ArrayToCSV(TableNames,<span class="hljs-string">#13#10</span>));
  (...)
</code></pre>
<p>  现在我们通过远程服务连接到数据库，并在<code>TComboBox</code>中检索表名。</p>
<p>  也可以执行特定的SQL语句：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TMainForm</span>.<span class="hljs-title">btnExecuteClick</span><span class="hljs-params">(Sender: TObject)</span>;</span>
<span class="hljs-keyword">var</span> SQL: RawUTF8;
<span class="hljs-keyword">begin</span>
  SQL := trim(StringToUTF8(mmoQuery.Text));
  Screen.Cursor := crHourGlass;
  <span class="hljs-keyword">try</span>
    <span class="hljs-keyword">try</span>
      <span class="hljs-keyword">if</span> isSelect(pointer(SQL)) <span class="hljs-keyword">then</span> <span class="hljs-keyword">begin</span>
        fTableJSON := fService.Execute(SQL,True,False);
        TSQLTableToGrid.Create(drwgrdData,
          TSQLTableJSON.Create([],SQL,pointer(fTableJSON),Length(fTableJSON)),fClient);
      <span class="hljs-keyword">end</span> <span class="hljs-keyword">else</span>
        fService.Execute(SQL,False,False);
    <span class="hljs-keyword">except</span>
      <span class="hljs-keyword">on</span> E: Exception <span class="hljs-keyword">do</span>
        ShowException(E);
    <span class="hljs-keyword">end</span>;
  <span class="hljs-keyword">finally</span>
    Screen.Cursor := crDefault;
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  这里，来自<code>mORMotUI</code>单元的<code>TSQLTableToGrid.Create()</code>将把返回的数据“注入”到标准的<code>TDrawGrid</code>，使用<code>TSQLTableJSON</code>实例来反序列化返回的JSON内容。</p>
<p>  请注意，如果出现任何异常（连接失败或服务端错误，如错误的SQL语句），<code>ShowExecption()</code>方法将用于通知用户适当的信息。</p>
<h2 id="toc_28">16.7. 异步回调<a class="vnote-anchor" href="#toc_28" data-anchor-icon="#"></a></h2>
<p>  在发布SOA服务时，大多数都定义为无状态的典型的查询/响应模式，请参阅<a href="">面向服务的体系结构（SOA）</a>。这与框架提出的<a href="">基于接口的客户端-服务端服务</a>的RESTful方法完全吻合。</p>
<p>  但是，客户端应用程序（或服务）可能需要知道给定服务的状态。在纯无状态实现中，它必须向服务器查询所有状态更改，即所有挂起的通知，这称为轮询。</p>
<p>  轮询可能发生在例如情况：</p>
<ul>
<li>当要在服务端处理耗时的工作时。在这种情况下，客户端不能等待它完成，也不能在HTTP连接上引发超时：作为一种解决方法，客户端可以继续工作，然后使用计时器和专用方法调用定期询问其进度状态；</li>
<li>当要从服务器端通知不可预测的事件时。在这种情况下，客户应定期（使用计时器，例如每秒）询问任何待决事件，然后对此作出反应。</li>
</ul>
<p>  因此，它可能看起来是首选的，并且在某些情况下是必要的，能够让服务器在没有任何优先查询的情况下通知一个或多个客户端，也不需要客户端计时器：</p>
<ul>
<li>轮询在客户端和服务器端都可能消耗大量资源，并增加一些不必要的延迟；</li>
<li>如果需要即时通知，可能需要某种“长轮询”算法，如果没有发生任何事件，服务器在返回通知状态之前需等待很长时间，这种情况下，需要专用连接，新增一个REST连接；</li>
<li>在事件驱动的系统中，许多消息被发送到客户端，优选适当的发布/订阅机制，否则轮询方法的复杂性可能会增加并变得低效且不可维护；</li>
<li>可能需要显式推送通知，如当客户端可能发送许多与一组复杂参数相关的潜在事件时。</li>
</ul>
<p>  因此，我们的mORMot框架能够轻松地在WebSockets上实现异步回调，将回调定义为服务方法定义中的接口参数，请参阅<a href="">服务方法参数</a>。</p>
<h3 id="toc_29">16.7.1. WebSockets支持<a class="vnote-anchor" href="#toc_29" data-anchor-icon="#"></a></h3>
<p>  根据定义，HTTP连接是无状态且单向的，即客户端向服务器发送请求，服务器响应回复。没有来自客户端的事先请求，就没有办法让服务器向客户端发送消息。</p>
<p>  WebSockets是一种通信协议，能够将常规HTTP连接升级为双向通信线路。在安全握手之后，通过应用程序定义的协议上的一组轻量级框架，可以直接访问底层TCP/IP套接字，而无需HTTP开销。</p>
<p>  <code>SynBidirSock.pas</code>单元实现服务器和客户端WebSockets底层通信。</p>
<p>  <code>TWebSocketProtocol</code>类定义了一个抽象的WebSockets协议，目前实现为几个类：</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-1" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 640 292" style="max-width:640px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-1 .node&gt;rect { ; }
#mermaid-diagram-1 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-1 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-1 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-1 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M239.5,64L239.5,89L308.1170212765957,114" marker-end="url(#arrowhead14)" style="fill:none"></path><defs><marker id="arrowhead14" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M497.5,64L497.5,89L428.8829787234043,114" marker-end="url(#arrowhead15)" style="fill:none"></path><defs><marker id="arrowhead15" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M120,158L120,183L186.0904255319149,208" marker-end="url(#arrowhead16)" style="fill:none"></path><defs><marker id="arrowhead16" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M368.5,158L368.5,183L302.4095744680851,208" marker-end="url(#arrowhead17)" style="fill:none"></path><defs><marker id="arrowhead17" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="A1" transform="translate(239.5,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-105.5" y="-22" width="211" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-95.5,-12)"><foreignObject width="191" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TWebSocketProtocolBinary</div></foreignObject></g></g></g><g class="node cyan" id="B2" transform="translate(368.5,136)" style="opacity: 1;"><rect rx="0" ry="0" x="-98.5" y="-22" width="197" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-88.5,-12)"><foreignObject width="177" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TWebSocketProtocolRest</div></foreignObject></g></g></g><g class="node cyan" id="A2" transform="translate(497.5,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-102.5" y="-22" width="205" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-92.5,-12)"><foreignObject width="185" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TWebSocketProtocolJSON</div></foreignObject></g></g></g><g class="node cyan" id="B1" transform="translate(120,136)" style="opacity: 1;"><rect rx="0" ry="0" x="-100" y="-22" width="200" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-90,-12)"><foreignObject width="180" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TWebSocketProtocolChat</div></foreignObject></g></g></g><g class="node cyan" id="C" transform="translate(244.25,230)" style="opacity: 1;"><rect rx="0" ry="0" x="-83.5" y="-22" width="167" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-73.5,-12)"><foreignObject width="147" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TWebSocketProtocol</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  对于基于接口的客户端-服务端服务，我们仍然需要发出RESTful请求，因此基本的WebSockets框架已得到增强，可支持<code>TWebSocketProtocolRest</code> REST兼容协议，能够为REST查询和异步通知使用同一个连接。</p>
<p>  您的SOA应用程序有两个类：</p>
<ul>
<li><code>TWebSocketProtocolJSON</code>作为“纯粹的”JSON轻量协议；</li>
<li><code>TWebSocketProtocolBinary</code>作为二进制专用协议，具有可选的帧压缩和AES加密（使用AES-NI硬件指令，如果可用）。</li>
</ul>
<p>  实际上，在服务端，您将通过指定<code>useBidirSocket</code>类型来启动TSQLHttpServer：</p>
<pre><code class="lang-pascal hljs"> HttpServer := TSQLHttpServer.Create(<span class="hljs-string">'8888'</span>,[Server],<span class="hljs-string">'+'</span>,useBidirSocket);
</code></pre>
<p>  它将实例化一个<code>TWebSocketServer</code>HTTP服务器，如<code>mORMotHttpServer.pas</code>中所定义，基于套接字API，能够将HTTP协议升级到WebSockets。 我们的<a href="">高性能http.sys服务</a>尚无法切换到WebSockets，在API级别，它至少需要Windows 8或Windows 2012 Server。</p>
<p>  然后使用对称加密密钥为<code>TWebSocketProtocolBinary</code>协议启用WebSockets：</p>
<pre><code class="lang-pascal hljs">HttpServer.WebSocketsEnable(Server,<span class="hljs-string">'encryptionkey'</span>);
</code></pre>
<p>  在客户端，您将使用<code>mORMotHttpClient.pas</code>中定义的<code>TSQLHttpClientWebsockets</code>实例，然后显式升级连接以使用WebSockets（因为默认情况下，它仅使用HTTP协议）：</p>
<pre><code class="lang-pascal hljs">  Client := TSQLHttpClientWebsockets.Create(<span class="hljs-string">'127.0.0.1'</span>,<span class="hljs-string">'8888'</span>,TSQLModel.Create([]));
  Client.WebSocketsUpgrade(<span class="hljs-string">'encryptionkey'</span>);
</code></pre>
<p>  协议细节应该与服务器上的协议细节匹配，即通过我们的二进制协议进行<code>'encryptionkey'</code>加密。</p>
<p>  升级到WebSockets后，您可以像往常一样使用常规REST命令：</p>
<pre><code class="lang-pascal hljs">  Client.ServerTimestampSynchronize;
</code></pre>
<p>  除了<a href="">基于接口的客户端-服务端服务</a>定义的常规查询/响应命令之外，您还可以使用服务方法的接口参数定义回调。</p>
<p>  客户端和服务端都将使用WebSockets框架进行通信，使用心跳（通过ping/pong框架）保持活动连接，并从任何一侧清理关闭连接。您可以使用<code>TSQLHttpServer.WebSocketsEnable()</code>返回的<code>TWebSocketServerRest</code>实例的<code>Settings</code>属性自定义服务端的底层WebSockets协议（例如超时或心跳）。 <code>TSQLHttpClientWebsockets.WebSockets.Settings</code>属性将在客户端支持相同的属性。</p>
<p>  我们从回归测试和内部基准测试中观察到，使用我们的WebSockets可能比常规HTTP更快，因为它的帧将被发送一次，而HTTP头和主体不会在同一个TCP数据包中发送，并且压缩可用于整个帧，而HTTP报文头不会被压缩。使用强AES加密的能力将使这种通信方式比普通HTTP更安全，即使HTTP通过AES进行了加密也是如此。</p>
<h4 id="toc_30">16.7.1.1. 长进程的"Saga"回调通知<a class="vnote-anchor" href="#toc_30" data-anchor-icon="#"></a></h4>
<p>  一个例子比100次言谈要好。</p>
<p>  让我们看一下<code>SQLite3\Samples\31 - WebSockets</code>子文件夹的<code>Project31LongWorkServer.dpr</code>和<code>Project31LongWorkClient.dpr</code>示例。他们将实现客户端-服务器应用，其中客户端在服务端启动长进程，然后在进程完成时通知成功或失败。</p>
<p>  这种模式在SOA世界中很常见，也称为"saga"，请参阅http://www.rgoarchitects.com/Files/SOAPatterns/Saga.pdf ，但在实践中，可能很难安全轻松地实现它。让我们看看我们的框架如何轻松地编写。</p>
<p>  首先，我们在共享的<code>Project31LongWorkCallbackInterface.pas</code>单元中定义要使用的接口：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  ILongWorkCallback = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{425BF199-19C7-4B2B-B1A4-A5BE7A9A4748}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">WorkFinished</span><span class="hljs-params">(<span class="hljs-keyword">const</span> workName: <span class="hljs-keyword">string</span>; timeTaken: integer)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">WorkFailed</span><span class="hljs-params">(<span class="hljs-keyword">const</span> workName, error: <span class="hljs-keyword">string</span>)</span>;</span>
  <span class="hljs-keyword">end</span>;

  ILongWorkService = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{09FDFCEF-86E5-4077-80D8-661801A9224A}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">StartWork</span><span class="hljs-params">(<span class="hljs-keyword">const</span> workName: <span class="hljs-keyword">string</span>; <span class="hljs-keyword">const</span> onFinish: ILongWorkCallback)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TotalWorkCount</span>:</span> Integer;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  唯一具体的定义是<code>ILongWorkService.StartWork()</code>方法的<code>const onFinish：ILongWorkCallback</code>参数。客户端将创建一个<code>ILongWorkCallback</code>的实现类，然后将其指定为此方法的参数。在服务端，将实现<code>ILongWorkCallback</code>“假”类，然后将在执行任何方法时使用完全相同的WebSockets连接回调客户端。</p>
<p>  如您所见，单个回调接口实例可能有多个方法，带有自己的参数集（此处为<code>WorkFinished</code>和<code>WorkFailed</code>），因此回调可能非常具有表现力。序列化后将传输任何类型的常用参数：字符串，整数，甚至记录，动态数组，<code>TSQLRecord</code>或<code>TPersistent</code>值。</p>
<p>  当客户端发布<code>ILongWorkCallback</code>实例时，将通知服务器，以便任何进一步的通知都不会产生连接错误。我们稍后会看到如何处理这些事件。</p>
<h4 id="toc_31">16.7.1.2. 客户端使用服务<a class="vnote-anchor" href="#toc_31" data-anchor-icon="#"></a></h4>
<p>  客户端可以这样连接到服务器（有关完整详细信息，请参阅<code>Project31LongWorkClient.dpr</code>示例源代码，包括错误处理）：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> Client: TSQLHttpClientWebsockets;
    workName: <span class="hljs-keyword">string</span>;
    Service: ILongWorkService;
    callback: ILongWorkCallback;
<span class="hljs-keyword">begin</span>
  Client := TSQLHttpClientWebsockets.Create(<span class="hljs-string">'127.0.0.1'</span>,<span class="hljs-string">'8888'</span>,TSQLModel.Create([]));
  Client.WebSocketsUpgrade(PROJECT31_TRANSMISSION_KEY);
  Client.ServiceDefine([ILongWorkService],sicShared);
  Client.Services.Resolve(ILongWorkService,Service);
</code></pre>
<p>  然后我们使用专用类定义我们的回调：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TLongWorkCallback</span> = <span class="hljs-keyword">class</span>(TInterfacedCallback,ILongWorkCallback)
  <span class="hljs-keyword">protected</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">WorkFinished</span><span class="hljs-params">(<span class="hljs-keyword">const</span> workName: <span class="hljs-keyword">string</span>; timeTaken: integer)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">WorkFailed</span><span class="hljs-params">(<span class="hljs-keyword">const</span> workName, error: <span class="hljs-keyword">string</span>)</span>;</span>
  <span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TLongWorkCallback</span>.<span class="hljs-title">WorkFailed</span><span class="hljs-params">(<span class="hljs-keyword">const</span> workName, error: <span class="hljs-keyword">string</span>)</span>;</span>
<span class="hljs-keyword">begin</span>
  writeln(<span class="hljs-string">#13</span><span class="hljs-string">'Received callback WorkFailed('</span>,workName,<span class="hljs-string">') with message "'</span>,error,<span class="hljs-string">'"'</span>);
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TLongWorkCallback</span>.<span class="hljs-title">WorkFinished</span><span class="hljs-params">(<span class="hljs-keyword">const</span> workName: <span class="hljs-keyword">string</span>;
  timeTaken: integer)</span>;</span>
<span class="hljs-keyword">begin</span>
  writeln(<span class="hljs-string">#13</span><span class="hljs-string">'Received callback WorkFinished('</span>,workName,<span class="hljs-string">') in '</span>,timeTaken,<span class="hljs-string">'ms'</span>);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  然后我们将这种回调指定为参数以启动长期工作：</p>
<pre><code class="lang-pascal hljs">    callback := TLongWorkCallback.Create(Client,ILongWorkCallback);
    <span class="hljs-keyword">try</span>
      <span class="hljs-keyword">repeat</span>
        readln(workName);
        <span class="hljs-keyword">if</span> workName=<span class="hljs-string">''</span> <span class="hljs-keyword">then</span>
          <span class="hljs-keyword">break</span>;
        Service.StartWork(workName,callback);
      <span class="hljs-keyword">until</span> false;
    <span class="hljs-keyword">finally</span>
      callback := <span class="hljs-keyword">nil</span>; <span class="hljs-comment">// the server will be notified and release its "fake" class</span>
      Service := <span class="hljs-keyword">nil</span>;  <span class="hljs-comment">// release the service local instance BEFORE Client.Free</span>
    <span class="hljs-keyword">end</span>;
</code></pre>
<p>  正如您所看到的，客户端能够启动一个或多个工作进程，然后期望在其回调接口实例上结束通知，而不显式轮询服务器的状态，因为连接已通过调用<code>TSQLHttpClientWebsockets.WebSocketsUpgrade()</code>将连接升级到WebSockets。</p>
<h4 id="toc_32">16.7.1.3. 服务端实现<a class="vnote-anchor" href="#toc_32" data-anchor-icon="#"></a></h4>
<p>  服务器将定义工作线程（有关完整详细信息，请参阅<code>Project31LongWorkServer.dpr</code>示例源代码）：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TLongWorkServiceThread</span> = <span class="hljs-keyword">class</span>(TThread)
  <span class="hljs-keyword">protected</span>
    fCallback: ILongWorkCallback;
    fWorkName: <span class="hljs-keyword">string</span>;
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Execute</span>;</span> <span class="hljs-keyword">override</span>;
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">Create</span><span class="hljs-params">(<span class="hljs-keyword">const</span> workName: <span class="hljs-keyword">string</span>; <span class="hljs-keyword">const</span> callback: ILongWorkCallback)</span>;</span>
  <span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">TLongWorkServiceThread</span>.<span class="hljs-title">Create</span><span class="hljs-params">(<span class="hljs-keyword">const</span> workName: <span class="hljs-keyword">string</span>;
  <span class="hljs-keyword">const</span> callback: ILongWorkCallback)</span>;</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">inherited</span> Create(false);
  fCallback := Callback;
  fWorkName := workName;
  FreeOnTerminate := true;
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TLongWorkServiceThread</span>.<span class="hljs-title">Execute</span>;</span>
<span class="hljs-keyword">var</span> tix: Int64;
<span class="hljs-keyword">begin</span>
  tix := GetTickCount64;
  Sleep(<span class="hljs-number">5000</span>+Random(<span class="hljs-number">1000</span>)); <span class="hljs-comment">// some hard work</span>
  <span class="hljs-keyword">if</span> Random(<span class="hljs-number">100</span>)&gt;<span class="hljs-number">20</span> <span class="hljs-keyword">then</span>
    fCallback.WorkFinished(fWorkName,GetTickCount64-tix) <span class="hljs-keyword">else</span>
    fCallback.WorkFailed(fWorkName,<span class="hljs-string">'expected random failure'</span>);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  回调将作为<code>ILongWorkCallback</code>接口实例提供，然后存储在<code>fCallback</code>保护字段中用于下一步的通知。</p>
<p>  一些工作是在<code>TLongWorkServiceThread.Execute</code>方法中完成的（这里只是一个超过5秒的<code>Sleep()</code>），并在处理结束时调用<code>ILongWorkCallback</code>接口方法发送成功或失败（取决于此假进程类中的随机）通知。。</p>
<p>  以下类将在服务器端定义，实现和注册<code>ILongWorkService</code>服务：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TLongWorkService</span> = <span class="hljs-keyword">class</span>(TInterfacedObject,ILongWorkService)
  <span class="hljs-keyword">protected</span>
    fTotalWorkCount: Integer;
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">StartWork</span><span class="hljs-params">(<span class="hljs-keyword">const</span> workName: <span class="hljs-keyword">string</span>; <span class="hljs-keyword">const</span> onFinish: ILongWorkCallback)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TotalWorkCount</span>:</span> Integer;
  <span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TLongWorkService</span>.<span class="hljs-title">StartWork</span><span class="hljs-params">(<span class="hljs-keyword">const</span> workName: <span class="hljs-keyword">string</span>;
  <span class="hljs-keyword">const</span> onFinish: ILongWorkCallback)</span>;</span>
<span class="hljs-keyword">begin</span>
  InterlockedIncrement(fTotalWorkCount);
  TLongWorkServiceThread.Create(workName,onFinish);
<span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TLongWorkService</span>.<span class="hljs-title">TotalWorkCount</span>:</span> Integer;
<span class="hljs-keyword">begin</span>
  result := fTotalWorkCount;
<span class="hljs-keyword">end</span>;

<span class="hljs-keyword">var</span> HttpServer: TSQLHttpServer;
    Server: TSQLRestServerFullMemory;
<span class="hljs-keyword">begin</span>
  Server := TSQLRestServerFullMemory.CreateWithOwnModel([]);
  Server.ServiceDefine(TLongWorkService,[ILongWorkService],sicShared);
  HttpServer := TSQLHttpServer.Create(<span class="hljs-string">'8888'</span>,[Server],<span class="hljs-string">'+'</span>,useBidirSocket);
  HttpServer.WebSocketsEnable(Server,PROJECT31_TRANSMISSION_KEY);
...
</code></pre>
<p>  这些方法的目的只是从客户端请求创建和启动<code>TLongWorkServiceThread</code>进程，然后在<code>sicShared</code>服务实例中维护已启动工作的总数 - 请参阅<a href="">实例生命周期实现</a>，在<code>useBidirSocket</code>类型的HTTP服务器中。</p>
<p>  我们必须显式调用<code>TSQLHttpServer.WebSocketsEnable()</code>，以便此服务器能够使用我们的二进制框架升级到我们的WebSockets协议，并使用与客户端相同的对称加密密钥，在示例中为<code>PROJECT31_TRANSMISSION_KEY</code>常量共享，但可以安全地存放在两侧。</p>
<h3 id="toc_33">16.7.2. 发布-订阅消息<a class="vnote-anchor" href="#toc_33" data-anchor-icon="#"></a></h3>
<p>  在事件驱动的体系结构中，发布-订阅消息传递模式是让发送者（称为发布者）向其接收者（称为订阅者）发送消息的一种方式，而不需要知道这些订阅者是谁。在实践中，订户将表达对一组消息的兴趣，一旦消息被通知，该消息将由发布者发送给给定消息的所有订户。</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-2" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 481 222" style="max-width:481px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-2 .node&gt;rect { ; }
#mermaid-diagram-2 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-2 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-2 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-2 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M188,57.97133757961784L73.5,101L73.5,138" marker-end="url(#arrowhead28)" style="fill:none"></path><defs><marker id="arrowhead28" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M230.5,64L230.5,101L230.5,138" marker-end="url(#arrowhead29)" style="fill:none"></path><defs><marker id="arrowhead29" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M273,57.97133757961784L387.5,101L387.5,138" marker-end="url(#arrowhead30)" style="fill:none"></path><defs><marker id="arrowhead30" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(73.5,101)" style="opacity: 1;"><g transform="translate(-19.5,-12)" class="label"><foreignObject width="39" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">Event</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(387.5,101)" style="opacity: 1;"><g transform="translate(-19.5,-12)" class="label"><foreignObject width="39" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">Event</span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="A" transform="translate(230.5,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-42.5" y="-22" width="85" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-32.5,-12)"><foreignObject width="65" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Publisher</div></foreignObject></g></g></g><g class="node cyan" id="B" transform="translate(73.5,160)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-22" width="107" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-12)"><foreignObject width="87" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Subscriber 1</div></foreignObject></g></g></g><g class="node cyan" id="C" transform="translate(230.5,160)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-22" width="107" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-12)"><foreignObject width="87" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Subscriber 2</div></foreignObject></g></g></g><g class="node cyan" id="D" transform="translate(387.5,160)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-22" width="107" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-12)"><foreignObject width="87" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Subscriber 3</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  在通过接口实现的客户端-服务端服务中，消息在接口类型中收集，每个消息定义为单个方法，其内容是方法参数。</p>
<p>  大多数SOA替代方案（在Java或C＃中）确实需要消息的类定义。 我们的KISS方法只使用方法参数值作为消息定义。</p>
<p>  要维护订阅者列表，最简单的方法是在发布者端存储动态的接口实例数组。</p>
<h4 id="toc_34">16.7.2.1. 定义接口<a class="vnote-anchor" href="#toc_34" data-anchor-icon="#"></a></h4>
<p>  我们现在将实现一个简单的聊天服务，能够让多个客户端一起通信，向所有其他连接的实例广播任何消息。 此示例也位于<code>SQLite3\Samples\31 - WebSockets</code>子文件夹中，如<code>Project31ChatServer.dpr</code>和<code>Project31ChatClient.dpr</code>。</p>
<p>  所以你首先定义回调接口和服务接口：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  IChatCallback = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{EA7EFE51-3EBA-4047-A356-253374518D1D}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">NotifyBlaBla</span><span class="hljs-params">(<span class="hljs-keyword">const</span> pseudo, msg: <span class="hljs-keyword">string</span>)</span>;</span>
  <span class="hljs-keyword">end</span>;

  IChatService = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{C92DCBEA-C680-40BD-8D9C-3E6F2ED9C9CF}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Join</span><span class="hljs-params">(<span class="hljs-keyword">const</span> pseudo: <span class="hljs-keyword">string</span>; <span class="hljs-keyword">const</span> callback: IChatCallback)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">BlaBla</span><span class="hljs-params">(<span class="hljs-keyword">const</span> pseudo,msg: <span class="hljs-keyword">string</span>)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">CallbackReleased</span><span class="hljs-params">(<span class="hljs-keyword">const</span> callback: IInvokable; <span class="hljs-keyword">const</span> interfaceName: RawUTF8)</span>;</span>
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  <code>IChatService</code>服务的主要命令是<code>BlaBla()</code>，它应该通过<code>IChatCallback.NotifyBlabla()</code>事件传播到已加入对话的所有客户端实例。</p>
<p>  这些接口类型将由服务器端和客户端共享，位于公共<code>Project31ChatCallbackInterface.pas</code>单元中。 该定义非常接近我们在使用“Saga”回调来通知长进程结束时所写的内容。 例如，如果有3个人加入聊天室，则应执行以下过程：</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-3" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 544 340" style="max-width:544px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-3 .node&gt;rect { ; }
#mermaid-diagram-3 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-3 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-3 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-3 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M262,64L262,101L262,138" marker-end="url(#arrowhead47)" style="fill:none"></path><defs><marker id="arrowhead47" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M208.5,177.73314606741573L84,219L84,256" marker-end="url(#arrowhead48)" style="fill:none"></path><defs><marker id="arrowhead48" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M262,182L262,219L262,256" marker-end="url(#arrowhead49)" style="fill:none"></path><defs><marker id="arrowhead49" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M315.5,177.73314606741573L440,219L440,256" marker-end="url(#arrowhead50)" style="fill:none"></path><defs><marker id="arrowhead50" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(262,101)" style="opacity: 1;"><g transform="translate(-26,-12)" class="label"><foreignObject width="52" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">BlaBla()</span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(84,219)" style="opacity: 1;"><g transform="translate(-47.5,-12)" class="label"><foreignObject width="95" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">NotifyBlaBla()</span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(440,219)" style="opacity: 1;"><g transform="translate(-47.5,-12)" class="label"><foreignObject width="95" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">NotifyBlaBla()</span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="A" transform="translate(262,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-71" y="-22" width="142" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-61,-12)"><foreignObject width="122" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">Any Joined Client</div></foreignObject></g></g></g><g class="node cyan" id="B" transform="translate(262,160)" style="opacity: 1;"><rect rx="0" ry="0" x="-53.5" y="-22" width="107" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-43.5,-12)"><foreignObject width="87" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">IChatService</div></foreignObject></g></g></g><g class="node cyan" id="C1" transform="translate(84,278)" style="opacity: 1;"><rect rx="0" ry="0" x="-64" y="-22" width="128" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-54,-12)"><foreignObject width="108" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">IChatCallback 1</div></foreignObject></g></g></g><g class="node cyan" id="C2" transform="translate(262,278)" style="opacity: 1;"><rect rx="0" ry="0" x="-64" y="-22" width="128" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-54,-12)"><foreignObject width="108" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">IChatCallback 2</div></foreignObject></g></g></g><g class="node cyan" id="C3" transform="translate(440,278)" style="opacity: 1;"><rect rx="0" ry="0" x="-64" y="-22" width="128" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-54,-12)"><foreignObject width="108" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">IChatCallback 3</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  唯一附加的方法是<code>IChatServer.CallbackReleased()</code>，按照惯例，当在客户端发布任何回调接口实例时，将在服务器端调用它。</p>
<p>  因此，<code>IChatService.Join()</code>方法将实现对聊天服务的订阅，而<code>IChatServer.CallbackReleased()</code>将在客户端回调实例被释放时被调用（即，当其变量被赋值为nil时）， 取消订阅聊天服务。</p>
<h4 id="toc_35">16.7.2.2. 编写发布者<a class="vnote-anchor" href="#toc_35" data-anchor-icon="#"></a></h4>
<p>  在服务器端，每次调用<code>IChatService.Join()</code>都会订阅一个内部连接列表，只需存储为<code>IChatCallback</code>数组：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TChatService</span> = <span class="hljs-keyword">class</span>(TInterfacedObject,IChatService)
  <span class="hljs-keyword">protected</span>
    fConnected: <span class="hljs-keyword">array</span> <span class="hljs-keyword">of</span> IChatCallback;
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Join</span><span class="hljs-params">(<span class="hljs-keyword">const</span> pseudo: <span class="hljs-keyword">string</span>; <span class="hljs-keyword">const</span> callback: IChatCallback)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">BlaBla</span><span class="hljs-params">(<span class="hljs-keyword">const</span> pseudo,msg: <span class="hljs-keyword">string</span>)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">CallbackReleased</span><span class="hljs-params">(<span class="hljs-keyword">const</span> callback: IInvokable; <span class="hljs-keyword">const</span> interfaceName: RawUTF8)</span>;</span>
  <span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TChatService</span>.<span class="hljs-title">Join</span><span class="hljs-params">(<span class="hljs-keyword">const</span> pseudo: <span class="hljs-keyword">string</span>;
  <span class="hljs-keyword">const</span> callback: IChatCallback)</span>;</span>
<span class="hljs-keyword">begin</span>
  InterfaceArrayAdd(fConnected,callback);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  <code>SynCommons.pas</code>中定义的<code>InterfaceArrayAdd()</code>函数是任何动态接口实例数组的简单包装器，因此您可以使用它或相关的<code>InterfaceArrayFind()</code>或<code>InterfaceArrayDelete()</code>函数来维护订阅列表。</p>
<p>  然后，只需调用<code>IChatCallback.BlaBla()</code>方法，就可以向所有连接的客户端广播<code>IChatService.BlaBla()</code>方法的远程调用：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TChatService</span>.<span class="hljs-title">BlaBla</span><span class="hljs-params">(<span class="hljs-keyword">const</span> pseudo,msg: <span class="hljs-keyword">string</span>)</span>;</span>
<span class="hljs-keyword">var</span> i: integer;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span> <span class="hljs-keyword">to</span> high(fConnected) <span class="hljs-keyword">do</span>
    fConnected[i].NotifyBlaBla(pseudo,msg);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  请注意，循环中对<code>IChatCallback.BlaBla()</code>的每次调用都将通过WebSockets以异步和非阻塞方式进行，这样即使在大量客户端的情况下，<code>IChatService.BlaBla()</code>方法也不会阻塞。 在大量消息的情况下，框架甚至能够将推送通知消息收集到单个更大的消息中，以减少资源使用，参阅<a href="">实时同步</a>。</p>
<p>  如果您有点偏执，如果任何事件失败，您可以确保通知过程将继续：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TChatService</span>.<span class="hljs-title">BlaBla</span><span class="hljs-params">(<span class="hljs-keyword">const</span> pseudo,msg: <span class="hljs-keyword">string</span>)</span>;</span>
<span class="hljs-keyword">var</span> i: integer;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">for</span> i := high(fConnected) <span class="hljs-keyword">downto</span> <span class="hljs-number">0</span> <span class="hljs-keyword">do</span> <span class="hljs-comment">// downwards for InterfaceArrayDelete()</span>
    <span class="hljs-keyword">try</span>
      fConnected[i].NotifyBlaBla(pseudo,msg);
    <span class="hljs-keyword">except</span>
      InterfaceArrayDelete(fConnected,i); <span class="hljs-comment">// unsubscribe the callback on failure</span>
    <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  这种更安全的实现将取消注册任何失败的回调。 如果通知引发了异常，则将确保不再通知此特定无效订户。 请注意，由于我们可能会动态减少<code>fConnected[]</code>数组大小，因此会向下处理循环，以避免任何访问冲突。</p>
<p>  在服务器端，服务实现已经注册如下：</p>
<pre><code class="lang-pascal hljs">  Server.ServiceDefine(TChatService,[IChatService],sicShared).
    SetOptions([],[optExecLockedPerInterface]);
</code></pre>
<p>  这里，设置了<code>optExecLockedPerInterface</code>选项，以便所有方法调用都是线程安全的：对内部<code>fConnected[]</code>列表的并发访问将受到锁的保护。 由于要维护全局连接列表，因此将服务生命周期定义为<code>sicShared</code>， 请参阅<a href="">实例生命周期实现</a>。</p>
<p>  当客户端回调实例被释放时（显式或连接断开），服务器将调用以下方法，因此可以用来取消订阅通知，只需从内部<code>fConnected[]</code>数组删除回调即可。</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TChatService</span>.<span class="hljs-title">CallbackReleased</span><span class="hljs-params">(<span class="hljs-keyword">const</span> callback: IInvokable; <span class="hljs-keyword">const</span> interfaceName: RawUTF8)</span>;</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> interfaceName=<span class="hljs-string">'IChatCallback'</span> <span class="hljs-keyword">then</span>
    InterfaceArrayDelete(fConnected,callback);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  实际上，框架将在服务的任何接口类型中识别以下方法定义（它将检查方法名称和方法参数）：</p>
<pre><code class="lang-pascal hljs">   <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">CallbackReleased</span><span class="hljs-params">(<span class="hljs-keyword">const</span> callback: IInvokable; <span class="hljs-keyword">const</span> interfaceName: RawUTF8)</span>;</span>
</code></pre>
<p>  当在客户端发布回调接口参数（在我们的例子中，<code>IChatCallback</code>）时，将使用相应的接口实例和类型名称作为参数调用此方法。 您不必明确调用客户端上的任何方法来取消订阅服务：将nil分配给回调变量，或者将拥有它的类实例释放为订阅者端的字段，将自动在发布者端注销它。</p>
<h4 id="toc_36">16.7.2.3. 使用订阅方服务<a class="vnote-anchor" href="#toc_36" data-anchor-icon="#"></a></h4>
<p>  在客户端，您实现<code>IChatCallback</code>回调接口：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TChatCallback</span> = <span class="hljs-keyword">class</span>(TInterfacedCallback,IChatCallback)
  <span class="hljs-keyword">protected</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">NotifyBlaBla</span><span class="hljs-params">(<span class="hljs-keyword">const</span> pseudo, msg: <span class="hljs-keyword">string</span>)</span>;</span>
  <span class="hljs-keyword">end</span>;

<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TChatCallback</span>.<span class="hljs-title">NotifyBlaBla</span><span class="hljs-params">(<span class="hljs-keyword">const</span> pseudo, msg: <span class="hljs-keyword">string</span>)</span>;</span>
<span class="hljs-keyword">begin</span>
  writeln(<span class="hljs-string">#13</span><span class="hljs-string">'@'</span>,pseudo,<span class="hljs-string">' '</span>,msg);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  <code>TInterfacedCallback</code>类型定义了一个<code>TInterfacedObject</code>子类，它将在释放时自动通知REST服务器。 通过将客户端<code>TSQLRest</code>实例提供给<code>TChatCallback.Create(0</code>构造函数，您将确保在客户端发布<code>TChatCallback/IChatCallback</code>实例时，将在服务器端执行<code>IChatService.CallbackReleased</code>方法。</p>
<p>  然后您订阅您的远程服务：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">var</span> Service: IChatService;
    callback: IChatCallback;
...
    Client.ServiceDefine([IChatService],sicShared);
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> Client.Services.Resolve(IChatService,Service) <span class="hljs-keyword">then</span>
      <span class="hljs-keyword">raise</span> EServiceException.Create(<span class="hljs-string">'Service IChatService unavailable'</span>);
...
      callback := TChatCallback.Create(Client,IChatCallback);
      Service.Join(pseudo,callback);
...
    <span class="hljs-keyword">try</span>
      <span class="hljs-keyword">repeat</span>
        readln(msg);
        <span class="hljs-keyword">if</span> msg=<span class="hljs-string">''</span> <span class="hljs-keyword">then</span>
          <span class="hljs-keyword">break</span>;
        Service.BlaBla(pseudo,msg);
      <span class="hljs-keyword">until</span> false;
    <span class="hljs-keyword">finally</span>
      callback := <span class="hljs-keyword">nil</span>; <span class="hljs-comment">// will unsubscribe from the remote publisher</span>
      Service := <span class="hljs-keyword">nil</span>;  <span class="hljs-comment">// release the service local instance BEFORE Client.Free</span>
    <span class="hljs-keyword">end</span>;
</code></pre>
<p>  通过将一些附加信息存储到接口实例（例如，要过滤的某个值，时间戳），您可以轻松实现更复杂的发布/订阅机制，包括过滤，生存时间或调谐广播。动态的专用记录数组 - 参见<a href="">TDynArray动态数组包装器</a>或类实例列表，可用于存储订阅者期望。</p>
<h4 id="toc_37">16.7.2.4. 订户多次重定向<a class="vnote-anchor" href="#toc_37" data-anchor-icon="#"></a></h4>
<p>  有时，在复杂的业务系统中，您将定义在多个解耦合代码中订阅同一服务消息。在DDD体系结构的基础结构层中实现中，通常会发生多个有界域上下文订阅同一事件源。</p>
<p>  最简单的实现路径是让各个部分从其自身侧进行注册。但它会导致与发布者的一些冗余流量。并且很可能最终会在订阅者端出现重复代码。</p>
<p>  您可以尝试<code>TSQLRest.MultiRedirect</code>并向远程服务注册一次，然后使用内部注册机制让业务逻辑的每个部分注册并使用事件。该方法返回一个<code>IMultiCallbackRedirect</code>接口，如果只需要一组事件，则允许使用一组可选的方法名称来注册子回调。</p>
<p>  请注意，子回调不需要从<code>TInterfacedCallback</code>类型继承：常规<code>TInterfacedObject</code>就足够了。如果它们引发异常，它们将自动从内部列表中注销。</p>
<h4 id="toc_38">16.7.2.5. 适当的线程实现<a class="vnote-anchor" href="#toc_38" data-anchor-icon="#"></a></h4>
<p>  mORMot多线程服务器将使用关键部分来保护共享数据，并避免潜在的竞争条件。但即使在客户端，回调也将在WebSockets传输线程的上下文中执行。在典型的微服务或事件驱动架构中，大多数节点同时是客户端和服务器，从而创建了对等的服务网络。因此，您应该通过保护对任何共享数据的访问来防止每个节点中的任何竞争条件。</p>
<p>  同样，如果您的回调触发另一个在另一个线程中共享相同关键部分的方法，则可能会遇到死锁问题。如果事件在用于保护共享资源的关键区域内触发回调，并且此回调运行阻塞REST请求，则将在传输线程的上下文中接收REST应答。如果此响应尝试访问相同的共享资源，则会与主要关键部分锁定发生冲突，因此执行将锁定。</p>
<p>  要实现回调过程的正确线程安全性，您可以遵循一些模式。</p>
<ul>
<li>使用几个小的关键部分，以尽可能小的粒度保护任何共享数据。您可以使用<code>TSynLocker</code>互斥锁或<code>TLockedDocVariant</code>无模式存储。</li>
<li>在回归测试中，确保使用并行请求运行多线程方案。您可以在<code>TSynParallelProcess</code>中找到运行并发客户端/服务器测试的简便方法。它将有助于找出最明显的实施问题。</li>
<li>根据定义，大多数死锁难以复制 - 它们是某种“Heisenbugs”。您可以确保正确记录回调过程，以便您能够跟踪生产中可能发生的任何死锁。</li>
<li>一个好主意可能是使用<code>TSQLRest.AsynchRedirect</code>在后台线程中收集所有非阻塞回调进程。此方法将通过伪类实现任何接口，该类将所有方法调用重定向到另一个接口的调用，作为后台线程中的FIFO。因此，您将确保所有回调过程将在单个线程中进行，从而避免大多数并发问题。作为副作用，内部FIFO将利用其他线程，因此可能有助于扩展您的系统。对于使用某些用户界面的客户端应用程序，请参阅下面的无锁替代方案。</li>
</ul>
<p>  多线程是性能的关键。但也很难正确实施。通过遵循这些简单的规则，您可以降低并发问题的风险。</p>
<h4 id="toc_39">16.7.2.6. 与UI/VCL交互<a class="vnote-anchor" href="#toc_39" data-anchor-icon="#"></a></h4>
<p>  如我们所述，所有回调通知都发生在传输线程中，即在与每个连接的客户端相对应的<code>TWebSocketProcessClientThread</code>实例中。</p>
<p>  您可能会像往常一样使用VCL <code>Synchronize()</code>方法将通知转发到UI层。不幸的是，这可能会引发一些意外的并发问题，例如：当在阻塞REST命令（例如<code>Service.BlaBla()</code>）期间接收到异步通知（例如<code>TChatCallback.NotifyBlaBla()</code>）时。阻塞命令中的<code>Synchronize</code>调用将阻止任何传入的异步通知并等待主线程可用，将阻止挂起的REST命令的响应...</p>
<p>  如果您尝试使用用户界面的随机群聊，以及与底层WebSockets超时相对应的404错误，即使在关闭应用程序时，您肯定会遇到这样的死锁。</p>
<p>  摆脱所有的<code>Synchronize()</code>用！使用Windows消息：它们安全，高效且快速。该框架可以用一行代码实现所有传入通知作为专用Windows消息转发：</p>
<pre><code class="lang-pascal hljs">Client.ServiceNotificationMethodViaMessages(MainForm.Handle,WM_SERVICENOTIFICATION);
</code></pre>
<p>  WM_SERVICENOTIFICATION应该定义为用户自定义消息：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">const</span>
 WM_SERVICENOTIFICATION = WM_USER; <span class="hljs-comment">// may be WM_USER+1, +2, ...</span>
</code></pre>
<p>  然后，TFormMain应该作为常规事件处理程序执行消息：</p>
<pre><code class="lang-pascal hljs"> <span class="hljs-title">TFormMain</span> = <span class="hljs-keyword">class</span>(TForm)
...
 <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ServiceNotification</span><span class="hljs-params">(<span class="hljs-keyword">var</span> Msg: TMessage)</span>;</span> <span class="hljs-keyword">message</span> WM_SERVICENOTIFICATION;
...
<span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">TFormMain</span>.<span class="hljs-title">ServiceNotification</span><span class="hljs-params">(<span class="hljs-keyword">var</span> Msg: TMessage)</span>;</span>
<span class="hljs-keyword">begin</span>
  TSQLRestClientURI.ServiceNotificationMethodExecute(Msg);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  由于这两行，回调将在主UI线程中异步执行，使用操作系统的优化消息队列，没有任何阻塞执行，也没有竞争条件。</p>
<h3 id="toc_40">16.7.3. 接口回调而不是类消息<a class="vnote-anchor" href="#toc_40" data-anchor-icon="#"></a></h3>
<p>  如果您与现有的客户端/服务端SOA解决方案（Delphi，Java，C＃甚至Go或其他框架中）进行比较，这种基于接口的回调机制看起来非常独特且易于使用。</p>
<p>  大多数面向事件的解决方案确实使用一组专用消息来传播事件，使用集中式消息总线（如MSMQ或JMS）或P2P方法（参见例如ZeroMQ或NanoMsg）。实际上，您需要为每条消息定义一个类，类字段是消息值。你会定义如一个类通知成功的进程，另一个类通知错误。 SOA服务最终将倾向于由大量单个类定义，并且在多个上下文中重复使用现有类。</p>
<p>  我们基于接口的方法允许聚合所有消息：</p>
<ul>
<li>在每个通知的单一接口类型中，即可能是每个服务操作；</li>
<li>每个事件使用一种方法；</li>
<li>使用方法参数定义事件值。</li>
</ul>
<p>  由于大多数时候都需要异步通知，因此方法参数将是单向的，即<code>const</code>。也可以定义阻塞请求，我们将在下面看到。并且进化的算法将透明地收集传出消息，以增强可伸缩性。</p>
<p>  在幕后，框架仍然会像其他系统一样通过IP套接字传输原始消息，但事件通知将受益于在服务端和客户端使用接口。</p>
<h4 id="toc_41">16.7.3.1. 使用服务和回调接口<a class="vnote-anchor" href="#toc_41" data-anchor-icon="#"></a></h4>
<p>  例如，您可以使用mORMot基于接口的方法定义以下通用服务和回调以从远程<code>camera</code>检索文件：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-comment">// define some custom types to make the implicit explicit</span>
  TCameraID = RawUTF8;
  TPictureID = RawUTF8;
  <span class="hljs-comment">// mORMot notifications using a callback interface definition</span>
  IMyCameraCallback = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{445F967F-79C0-4735-A972-0BED6CC63D1D}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Started</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Camera: TCameraID; <span class="hljs-keyword">const</span> Picture: TPictureID)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Progressed</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Camera: TCameraID; <span class="hljs-keyword">const</span> Picture: TPictureID;
      CurrentSize,TotalSize: cardinal)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Finished</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Camera: TCameraID; <span class="hljs-keyword">const</span> Picture: TPictureID;
      <span class="hljs-keyword">const</span> PublicURI: RawUTF8; TotalSize: cardinal)</span>;</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ErrorOccured</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Camera: TCameraID; <span class="hljs-keyword">const</span> Picture: TPictureID;
      <span class="hljs-keyword">const</span> MessageText: RawUTF8)</span>;</span>
  <span class="hljs-keyword">end</span>;
  <span class="hljs-comment">// mORMot main service, also defined as an interface</span>
  IMyCameraService = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{3CE61E74-A01D-41F5-A414-94F204F140E1}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TakePicture</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Camera: TCameraID; <span class="hljs-keyword">const</span> Callback: IMyCameraCallback)</span>:</span> TPictureID;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  深呼吸，并记住这两种类型定义作为参考。 单一看，我猜你确实得到了需要的<code>"Camera Service"</code>。 我们现在将与传统的基于消息的模式进行比较。</p>
<h4 id="toc_42">16.7.3.2. 经典消息事件<a class="vnote-anchor" href="#toc_42" data-anchor-icon="#"></a></h4>
<p>  使用基于类的消息类型的实现，您可能会定义一个包含所有潜在信息的类：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-comment">// a single class message will need a status</span>
  TMyCameraCallbackState = (
    ccsStarted, ccsProgressed, ccsFinished, ccsErrorOccured);
  <span class="hljs-comment">// the single class message</span>
  TMyCameraCallbackMessage = <span class="hljs-keyword">class</span>
  <span class="hljs-keyword">private</span>
    fCamera: TCameraID;
    fPicture: TPictureID;
    fTotalSize: cardinal;
    fMessageText: RawUTF8;
    fState: TMyCameraCallbackState;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> State: TMyCameraCallbackState <span class="hljs-keyword">read</span> fState <span class="hljs-keyword">write</span> fState;
    <span class="hljs-keyword">property</span> Camera: TCameraID <span class="hljs-keyword">read</span> fCamera <span class="hljs-keyword">write</span> fCamera;
    <span class="hljs-keyword">property</span> Picture: TPictureID <span class="hljs-keyword">read</span> fPicture <span class="hljs-keyword">write</span> fPicture;
    <span class="hljs-keyword">property</span> TotalSize: cardinal <span class="hljs-keyword">read</span> fTotalSize <span class="hljs-keyword">write</span> fTotalSize;
    <span class="hljs-keyword">property</span> MessageText: RawUTF8 <span class="hljs-keyword">read</span> fMessageText <span class="hljs-keyword">write</span> fMessageText;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  这个单一类很容易编写，但是使用通知会让人感到有点困惑。 哪个字段是哪个状态？ 客户端代码最终将包含一个巨大的<code>case aMessage.State of ...</code>语句块，存在潜在问题。 业务逻辑未出现在此类型定义中，易于编写，难以阅读和维护......</p>
<p>  为了使实现更接近SOLID设计原则，您可以定义一组类，如下所示：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-comment">// all classes will inherit from this one, to have common properties</span>
  TMyCameraCallbackAbstract = <span class="hljs-keyword">class</span>
  <span class="hljs-keyword">private</span>
    fCamera: TCameraID;
    fPicture: TPictureID;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> Camera: TCameraID <span class="hljs-keyword">read</span> fCamera <span class="hljs-keyword">write</span> fCamera;
    <span class="hljs-keyword">property</span> Picture: TPictureID <span class="hljs-keyword">read</span> fPicture <span class="hljs-keyword">write</span> fPicture;
  <span class="hljs-keyword">end</span>;
  <span class="hljs-comment">// message class when the picture acquisition starts</span>
  <span class="hljs-title">TMyCameraCallbackStarted</span> = <span class="hljs-keyword">class</span>(TMyCameraCallbackAbstract);
  <span class="hljs-comment">// message class when the picture is acquired</span>
  <span class="hljs-title">TMyCameraCallbackFinished</span> = <span class="hljs-keyword">class</span>(TMyCameraCallbackAbstract)
  <span class="hljs-keyword">private</span>
    fPublicURI: RawUTF8;
    fTotalSize: cardinal;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> TotalSize: cardinal <span class="hljs-keyword">read</span> fTotalSize <span class="hljs-keyword">write</span> fTotalSize;
    <span class="hljs-keyword">property</span> PublicURI: RawUTF8 <span class="hljs-keyword">read</span> fPublicURI <span class="hljs-keyword">write</span> fPublicURI;
  <span class="hljs-keyword">end</span>;
  <span class="hljs-comment">// message during picture download</span>
  <span class="hljs-title">TMyCameraCallbackProgressed</span> = <span class="hljs-keyword">class</span>(TMyCameraCallbackFinished)
  <span class="hljs-keyword">private</span>
    fCurrentSize: cardinal;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> CurrentSize: cardinal <span class="hljs-keyword">read</span> fCurrentSize <span class="hljs-keyword">write</span> fCurrentSize;
  <span class="hljs-keyword">end</span>;
  <span class="hljs-comment">// error message</span>
  <span class="hljs-title">TMyCameraCallbackErrorOccured</span> = <span class="hljs-keyword">class</span>(TMyCameraCallbackAbstract)
  <span class="hljs-keyword">private</span>
    fMessageText: RawUTF8;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> MessageText: RawUTF8 <span class="hljs-keyword">read</span> fMessageText <span class="hljs-keyword">write</span> fMessageText;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  继承使得这个类层次结构不像普通的“平面”类那样冗长，但它仍然比<code>IMyCameraCallback</code>类型定义的可读性低得多。</p>
<p>  在这两种情况下，这样的类定义使得难以猜测哪个消息与哪个服务匹配。您必须对命名约定非常小心和一致，并在明确的命名空间中解除服务定义。</p>
<p>  在实现SOA服务时，DDD的通用语言往往受到类定义（<code>getter</code>和<code>setter</code>）以及基于消息的通知的实现细节的污染：您的领域代码将与基础架构层的面向消息的性质相关联。我们将在下面看到接口回调如何以更清晰的方式帮助实现DDD的事件驱动模式。</p>
<h4 id="toc_43">16.7.3.3. 工作流程适配<a class="vnote-anchor" href="#toc_43" data-anchor-icon="#"></a></h4>
<p>  有时，可能有必要对某些意外事件做出反应。消费者可能需要更改生产者的工作流程，具体取决于某些业务规则，意外错误或最终用户交互。</p>
<p>  根据设计，基于消息的实现是异步的，非阻塞的：消息被发送并存储在消息代理/总线中，其内部处理循环将消息传递给所有订户。在这样的实现中，自然没有“反向”反馈消息。</p>
<p>  一种常见的模式是拥有一组专用的“回答/反馈”消息，以通知服务提供者状态变化 - 它带有潜在的竞争条件或意外的反弹现象，例如当您向现有事件驱动的系统添加节点时。</p>
<p>  另一种解决方案可以是为服务提供者定义明确的规则，例如何时调用该服务。您可以定义一组工作流，在运行时注入提供者/总线服务。它最终将倾向于打破单一责任原则，并将逻辑放在基础架构层中。</p>
<p>  另一方面，由于mORMot的回调是真正的接口方法，因此它们可能返回一些值（作为函数结果或<code>var</code>/<code>out</code>参数）。在服务器端，此类回调将阻止并等待客户端响应。</p>
<p>  所以通过写一个额外的方法，如：</p>
<pre><code class="lang-pascal hljs">  IMyCameraCallback = <span class="hljs-keyword">interface</span>(IInvokable)
  ...
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ShouldRetryIfBusy</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Camera: TCameraID; <span class="hljs-keyword">const</span> Picture: TPictureID)</span>:</span> boolean;
  ...
</code></pre>
<p>  ......您将能够实时实施任何所需的复杂工作流程调整。服务器端代码仍然是非常易读和高效的，没有复杂的管道，等待设置队列或状态机。</p>
<h4 id="toc_44">16.7.3.4. 接口带来的抽象和轻松<a class="vnote-anchor" href="#toc_44" data-anchor-icon="#"></a></h4>
<p>  作为额外的好处，与Delphi语言的集成显然是与实现无关的：在使用这样的接口类型定义时，您甚至不会使用框架。实际上，这是在服务端实现符合SOLID设计原则的回调的好方法，并且只有在必要时才让mORMot框架通过使用WebSockets以客户端/服务器方式发布此机制。</p>
<p>  可以在服务端使用相同的代码，没有传输或编码开销（通过直接接口实例调用），并且通过网络，优化使用资源和带宽（通过“假”接口调用和二进制/JSON）通过TCP/IP进行编码）。</p>
<p>  在服务器端，您的代码（尤其是您的领域代码）可以直接与作为接口类型在领域中定义的底层服务进行交互，并在基础结构层中实现。您可以在单个服务器可执行文件中托管域代码和基础结构代码，并将本地类实例直接分配为回调。这将最大限度地减少CPU和内存条件中的程序资源 - 这对任何业务系统来说都是非常有价值的目标。</p>
<p>  您可以在独立应用程序中重用应用程序和业务逻辑，从UI到应用程序界面的类似直接调用。根据需要，接口变量可以指向远程mORMot服务器，而不接触VCL/FMX代码。</p>
<p>  最后但最重要的是，使用接口将有助于使用Stub和mocks实现整个回调机制，例如：通过<a href="">调用跟踪</a>轻松进行单元测试。</p>
<p>  您还可以使用真正的本地回调类实例编写单元测试，这将比整个客户端/服务器堆栈更容易调试。一旦确定了系统失败的场景，您就可以使用专用测试重现它，即使是采用激进的多线程方式，然后使用调试器跟踪执行并找出问题的根本原因。</p>
<h2 id="toc_45">16.8. 实现细节<a class="vnote-anchor" href="#toc_45" data-anchor-icon="#"></a></h2>
<h3 id="toc_46">16.8.1. 错误处理<a class="vnote-anchor" href="#toc_46" data-anchor-icon="#"></a></h3>
<p>  通常，在Delphi应用程序中（与大多数高级语言类似），通过异常来处理错误。默认情况下，在基于接口的服务方法中，在服务端引发的任何异常都将被截获，并作为HTTP错误传输到客户端，然后触发安全但有些模糊的<code>EInterfaceFactoryException</code>异常，其中包含序列化为JSON的附加错误信息。</p>
<p>  您可能想知道为什么不在客户端直接传输和引发异常，就好像它们是在本地执行的一样。</p>
<p>  事实上，异常不是值对象，而是真正的类实例，具有一些方法和对其他对象的潜在内部引用。大多数情况下，它们与特定的执行上下文相关联，甚至与某些底层实现细节相关联。 Delphi异常甚至是非常具体的，并且不容易转换成JavaScript，Java或C＃等异常。</p>
<p>  实际上，重新创建和触发在服务端发生的相同<code>Exception</code>类的实例将导致客户端代码对服务端实现细节的强烈依赖性。例如，如果服务端引发<code>ESQLDBOracle</code>异常，则在另一端进行转换需要将客户端与整个<code>SynDBOracle.pas</code>单元链接起来，这当然不值得。 <code>ESQLDBOracle</code>异常本身包含一个指向Oracle语句实例的链接，该链接在通过线路转换时将丢失。一些客户端平台（例如移动或AJAX）甚至不知道Oracle数据库是什么......</p>
<p>  因此，异常不是序列化的良好候选者，也不是从服务端到客户端的每个值的传输。我们不会赞成向客户端直接处理异常。</p>
<p>  这就是为什么应该在服务器端更好地拦截异常，在服务方法中使用<code>try .. except</code>块，然后转换为特定于服务的低级DTO类型，然后作为错误代码显式地传输到客户端。</p>
<p>  第一条规则是触发异常应该是例外，正如其名称所述：例外。我的意思是，即使在输入错误的情况下，服务代码也不应在正常执行中引发异常。例如，错误的输入参数应该导致应用程序级错误，作为枚举项和/或一些其他（可能是文本）信息传输，但业务逻辑不应该引发任何异常。仅在低级意外事件（例如，SQL级别故障，GPF或访问冲突，与另一个可信内部服务的通信错误）的情况下，服务器端可能进入恐慌模式，并引发异常。请记住，<code>SynLog.pas</code>拦截了异常，并且可以通过我们的日志系统轻松记录：您将能够识别执行上下文，并找到问题的完整堆栈跟踪。但是，大多数常见错误应该在业务逻辑级别处理，甚至在每个服务层中定义。</p>
<p>  在实践中，您可以使用枚举，结合变体来获取其他结构化信息（作为字符串或更复杂的<code>TDocVariant</code>），以将错误传输到客户端。您可以在每一层定义专用类型，例如使用领域服务或应用程序服务的接口类型。</p>
<p>  请参阅<code>mORMotDDD.pas</code>中如何定义<code>ICQRSService</code>及其相关的<code>TCQRSResult</code>枚举：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  TCQRSResult =
    (cqrsSuccess, cqrsSuccessWithMoreData,
     cqrsUnspecifiedError, cqrsBadRequest,
     cqrsNotFound, cqrsNoMoreData, cqrsDataLayerError,
     cqrsInternalError, cqrsDDDValidationFailed,
     cqrsInvalidContent, cqrsAlreadyExists,
     ...

  ICQRSService = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{923614C8-A639-45AD-A3A3-4548337923C9}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetLastError</span>:</span> TCQRSResult;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GetLastErrorInfo</span>:</span> variant;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  <code>TCQRSResult</code>枚举的第一个<code>cqrsSuccess</code>项将是默认值（映射并传输JSON数字0），因此，如果存在一些stub或mock接口，假方法将按预期返回成功 - 请参阅<a href="">stub和mock</a>。</p>
<p>  当在服务方法中引发任何异常时，可以返回<code>TCQRSResult</code>枚举值作为结果，以便直接传输错误：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TDDDMonitoredDaemon</span>.<span class="hljs-title">Stop</span><span class="hljs-params">(<span class="hljs-keyword">out</span> Information: variant)</span>:</span> TCQRSResult;
...
<span class="hljs-keyword">begin</span>
  CqrsBeginMethod(qaNone,result);
  <span class="hljs-keyword">try</span>
....
    CqrsSetResult(cqrsSuccess,result);
  <span class="hljs-keyword">except</span>
    <span class="hljs-keyword">on</span> E: Exception <span class="hljs-keyword">do</span>
      CqrsSetResult(E,cqrsInternalError,result);
  <span class="hljs-keyword">end</span>;
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  但正如我们已经说过的那样，这种异常应该是例外。</p>
<p>  <code>mORMotDDD.pas</code>单元在<code>TCQRSQueryObject</code>抽象类中定义了一些受保护的方法来处理<code>ICQRSService</code>所期望的错误和异常。例如，<code>TCQRSQueryObject.CqrsSetResult()</code>方法将设置<code>result := cqrsInternalError</code>并序列化包含附加错误的内部变体中的<code>E: Exception</code>，可使用<code>ICQRSService.GetLastErrorInfo</code>进行检索。</p>
<p>  在发生灾难性故障时，异常对于中断进程非常有用，但它们不是通过远程服务传输错误的最佳方法。一些较新的语言（例如Google的Go）甚至不会在语言或RTL级别定义任何异常类型，而是依赖返回的值在执行上下文之间传输错误 - 请参阅https://golang.org/doc/faq#exceptions：在我们的客户端-服务端错误处理设计中，我们遵循相同的想法。</p>
<h3 id="toc_47">16.8.2. 安全<a class="vnote-anchor" href="#toc_47" data-anchor-icon="#"></a></h3>
<p>  正如<a href="">基于接口的客户端-服务端服务</a>的功能所述，使用客户端-服务端服务时可以拥有完整的安全模式。在面向服务的架构（SOA）中，保护客户端和服务端之间的消息对于保护数据至关重要。</p>
<p>  遵循mORMot的主要安全模式，可在多个级别实现安全性 ：</p>
<ul>
<li>过程安全，主要用于通信流，例如在客户端-服务端进程中使用HTTPS协议，或在HTTP内容编码中使用自定义算法；</li>
<li>在RESTful/URI身份验证级别，请参阅有关会话、组和用户概念的叙述；</li>
<li>通过接口或方法（服务/操作）级别的授权来允许或禁止给定操作。</li>
</ul>
<p>  我们现在讨论最后两点（认证和授权）。</p>
<p>  默认情况下，基于接口的服务的设置如下：</p>
<ul>
<li>所有服务（即所有接口）都需要有效验证认证方案（至少<code>TSQLRestServerAuthenticationWeak</code>），即客户端发起的轻会话，简而言之，显式认证是强制性的；</li>
<li>允许执行所有操作（即所有方法），简而言之，授权已启用并开放。</li>
</ul>
<p>  您可以通过与每个接口相对应的<code>TServiceFactoryServer</code>实例在服务器端进行这些设置（这是一个实现细节，因为在客户端调整它没有任何意义），您可以访问这些实例，如<code>TSQLRestServer.Services</code>属性。</p>
<p>  要禁用整个服务/接口的身份验证，可以使用与给定接口对应的<code>TServiceFactoryServer</code>实例的<code>ByPassAuthentication</code>属性。这应该是有用的，如对于不涉及任何敏感数据的简单Web服务（服务目录、返回公共信息、HTML内容服务等）。</p>
<p>  然后，为了在操作（方法）级别调整授权过程，<code>TServiceFactoryServer</code>提供以下方法来更改每个接口的安全策略：</p>
<ul>
<li><code>AllowAll()</code>和<code>Allow()</code>允许执行所有方法；</li>
<li><code>DenyAll()</code>和<code>Deny()</code>禁用执行所有方法；</li>
<li><code>AllowAllByID()</code>和<code>AllowByID()</code>允许ID组执行的方法；</li>
<li><code>DenyAllByID()</code>和<code>DenyByID()</code>禁止ID组执行方法；</li>
<li><code>AllowAllByName()</code>和<code>AllowByName()</code>允许组名执行方法；</li>
<li><code>DenyAllByName()</code>和<code>DenyByName()</code>禁止组名执行方法。</li>
</ul>
<p>  前四种方法的影响是全面的。后面的<code>*ByID()</code>四个方法接受一个身份验证组ID列表（即<code>TSQLAuthGroup.ID</code>值），而<code>*ByName()</code>方法将处理<code>TSQLAuthGroup.Ident</code>属性值。</p>
<p>  实际上，可以为特定的一组通过身份验证的用户授权执行。因此，您的服务可以提供一些基本功能，然后仅为管理员或主管启用高级功能。用户/组策略可在我们的RESTful身份验证方案中完全自定义，mORMot提供了一种通用且可互操作的安全模式。</p>
<p>  以下是提供的回归测试的一些摘录：</p>
<pre><code class="lang-pascal hljs"> (...)
  S := fClient.Server.Services[<span class="hljs-string">'Calculator'</span>] <span class="hljs-keyword">as</span> TServiceFactoryServer;
  Test([<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>,<span class="hljs-number">4</span>,<span class="hljs-number">5</span>],<span class="hljs-string">'by default, all methods are allowed'</span>);
  S.AllowAll;
  Test([<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>,<span class="hljs-number">4</span>,<span class="hljs-number">5</span>],<span class="hljs-string">'AllowAll should change nothing'</span>);
  S.DenyAll;
  Test([],<span class="hljs-string">'DenyAll will reset all settings'</span>);
  S.AllowAll;
  Test([<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>,<span class="hljs-number">4</span>,<span class="hljs-number">5</span>],<span class="hljs-string">'back to full acccess for everybody'</span>);
  S.DenyAllByID([GroupID]);
  Test([],<span class="hljs-string">'our current user shall be denied'</span>);
  S.AllowAll;
  Test([<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>,<span class="hljs-number">4</span>,<span class="hljs-number">5</span>],<span class="hljs-string">'restore allowed for everybody'</span>);
  S.DenyAllByID([GroupID+<span class="hljs-number">1</span>]);
  Test([<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>,<span class="hljs-number">4</span>,<span class="hljs-number">5</span>],<span class="hljs-string">'this group ID won''t affect the current user'</span>);
  S.DenyByID([<span class="hljs-string">'Add'</span>],GroupID);
  Test([<span class="hljs-number">2</span>,<span class="hljs-number">3</span>,<span class="hljs-number">4</span>,<span class="hljs-number">5</span>],<span class="hljs-string">'exclude a specific method for the current user'</span>);
  S.DenyByID([<span class="hljs-string">'totext'</span>],GroupID);
  Test([<span class="hljs-number">2</span>,<span class="hljs-number">3</span>,<span class="hljs-number">5</span>],<span class="hljs-string">'exclude another method for the current user'</span>);
 (...)
</code></pre>
<p>  在上面的回归测试代码中，<code>Test()</code>本地过程根据一组方法索引来验证<code>ICalculator</code>的相应方法（1=Add, 2=Multiply, 3=Subtract, 4=ToText...）。</p>
<p>  <code>GroupID</code>值使用如下代码检索：</p>
<pre><code class="lang-pascal hljs">  GroupID := fClient.MainFieldID(TSQLAuthGroup,<span class="hljs-string">'User'</span>);
</code></pre>
<p>  并且客户端上当前通过身份验证的用户已被定义为<code>'User'</code>组成员：</p>
<pre><code class="lang-pascal hljs">  fClient.SetUser(<span class="hljs-string">'User'</span>,<span class="hljs-string">'synopse'</span>); <span class="hljs-comment">// default user for Security tests</span>
</code></pre>
<p>  由于<code>TSQLRestServer.ServiceRegister</code>和<code>TSQLRestServer.ServiceDefine</code>方法返回第一个创建的<code>TServiceFactoryServer</code>实例，并且由于所有<code>Allow*</code>/ <code>AllowAll*</code>/ <code>Deny*</code>/ <code>DenyAll *</code>方法也返回<code>TServiceFactoryServer</code>实例，因此您可以在代码中使用某种“流畅的接口” 设置安全策略，如下：</p>
<pre><code class="lang-pascal hljs">  Server.ServiceDefine(TServiceCalculator,[ICalculator],sicShared).
    DenyAll.AllowAllByName([<span class="hljs-string">'Supervisor'</span>]);
</code></pre>
<p>  这将仅允许<code>Supervisor</code>用户组访问<code>ICalculator</code>方法。</p>
<h3 id="toc_48">16.8.3. 实现类类型<a class="vnote-anchor" href="#toc_48" data-anchor-icon="#"></a></h3>
<p>  大多数情况下，您的实现类将继承自<code>TInterfacedObject</code>。 如上所述，您实际上可以从任何普通的Delphi类继承：唯一的条件是它实现了需要的接口，并且具有GUID。</p>
<p>  但是，如果在类实例初始化期间需要进行特殊处理，则可以从<code>TInterfacedObjectWithCustomCreate</code>类继承，该类提供以下虚拟构造函数，可以使用自定义覆盖初始化：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-title">TInterfacedObjectWithCustomCreate</span> = <span class="hljs-keyword">class</span>(TInterfacedObject)
  <span class="hljs-keyword">public</span>
    <span class="hljs-comment">/// this virtual constructor will be called at instance creation</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">Create</span>;</span> <span class="hljs-keyword">virtual</span>;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  但是从SOA的角度来看，使用具有正确参数的专用方法来初始化您的实例是有意义的，如在<code>sicClientDriven</code>执行模式。请参阅<a href="">增强示例：远程SQL访问</a>实现<code>IRemoteSQL</code>服务的一些示例代码，在初始化<code>sicClientDriven</code>实例的所有其他方法之前调用专用的<code>Connect()</code>方法。</p>
<h3 id="toc_49">16.8.4. 服务端执行选项（线程）<a class="vnote-anchor" href="#toc_49" data-anchor-icon="#"></a></h3>
<p>  在服务端注册服务时，可以使用<code>TServiceFactoryServer.SetOptions()</code>方法定义某些选项以指定其执行细节。</p>
<p>  默认情况下，服务方法在接收它们的线程内调用。也就是说，当托管在多线程服务实例（如<code>TSQLite3HttpServer</code>或<code>TSQLRestServerNamedPipeResponse</code>）中时，方法上下文是可以并发的，除非定义了<code>sicSingle</code>或<code>sicPerThread</code>实例生命周期模式。它允许更好的响应时间和CPU使用，但缺点是方法实现必须是线程安全的。这就是为什么服务实现方法必须小心处理多线程安全的技术原因，如通过有目的地使用安全锁来实现多线程应用。</p>
<p>  可以使用以下执行选项：</p>
<table>
<thead>
<tr>
<th>TServiceMethodOptions</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>none（默认）</td>
<td>所有方法都是可重入的，并且应该编码为线程安全的</td>
</tr>
<tr>
<td>optExecLockedPerInterface</td>
<td>每个接口将被它自己的互斥锁保护/锁定</td>
</tr>
<tr>
<td>optExecLockedPerInterface<br>optFreeInMainThread</td>
<td>方法将在流程主线程中执行<br>接口将在进程主线程中释放</td>
</tr>
<tr>
<td>optExecInPerInterfaceThread<br>optExecInPerInterfaceThread</td>
<td>每个接口将在它自己的线程中执行它的方法<br>每个接口将在它自己的线程中被释放</td>
</tr>
</tbody>
</table>
<p>  当然，如果要在方法级调整执行，<code>SetOption()</code>接受方法名称的可选列表。</p>
<p>  设置<code>optExecLockedPerInterface</code>选项将锁定接口级的指定方法执行。也就是说，不可能同时执行两个相同接口的方法。此选项使用<code>TRTLCriticalSection</code>互斥锁，因此既安全又使用非常少的资源。但是它不能保证方法执行总是在同一个线程中发生：所以如果你需要一些每线程初始化/完成（如对于COM对象），你应该更好地使用其他选项。</p>
<p>  设置<code>optExecInMainThread</code>选项将强制在<code>RunningThread.Synchronize()</code>调用中调用指定的方法，它可以用于诸如，如果您的实现严重依赖COM对象，或者您希望确保您的代码能够正常工作，而无需担心线程安全，这可能很难处理。 <code>optFreeInMainThread</code>选项还将确保服务类实例将在主线程中释放（即通过<code>Synchronize</code>调用其<code>Free</code>方法）。由于所有接口都使用主线程，因此可能导致执行瓶颈。</p>
<p>  设置<code>optExecInPerInterfaceThread</code>选项将强制在专用于接口的线程（更准确地说，一个<code>TSynBackgroundThreadSQLRestServerProcedure</code>类，它将通知<code>TSQLSQLRestServer</code>以获取线程上下文）中调用指定的方法。相关的<code>optFreeInPerInterfaceThread</code>选项还将确保服务类实例将在同一个线程中释放：使用此线程模型非常方便，如维护基于<code>SynDB.pas</code>的专用数据库连接，或者初始化一些COM对象。</p>
<p>  例如，如果您希望在主线程中执行<code>TServiceCalculator</code>类的所有方法，则可以定义：</p>
<pre><code class="lang-pascal hljs"> Server.ServiceDefine(TServiceCalculator,[ICalculator],sicShared).
  SetOptions([],[optExecInMainThread]);
</code></pre>
<p>  或者，如果只需要保护<code>TServiceCalculator.Add</code>方法，您可以编写：</p>
<pre><code class="lang-pascal hljs"> Server.ServiceDefine(TServiceCalculator,[ICalculator],sicShared).
  SetOptions([<span class="hljs-string">'Add'</span>],[optExecInMainThread]);
</code></pre>
<p>  实际上，<code>SetOptions()</code>方法遵循类似于定义服务安全性的调用签名。</p>
<p>  为了获得最佳性能，您可以定义您的服务方法调用，无需任何锁定，依赖于<code>SynCommons.pas</code>中定义的一些方便的类，如<code>TAutoLocker</code>类或<code>TLockedDocVariant</code>类型的存储，以实现高效的多线程处理。</p>
<p>  类似的线程安全问题也适用于MVVM方法 。</p>
<h3 id="toc_50">16.8.5. 服务审计跟踪<a class="vnote-anchor" href="#toc_50" data-anchor-icon="#"></a></h3>
<p>  我们之前已经看到框架的ORM部分是如何提供<a href="">变更审计跟踪</a>。这是一种非常方便的存储数据状态变化的方法。另一方面，在各种现代SOA解决方案中，数据不再是中心，而是服务。有时，数据不会存储在您的服务器中，而是存储在第三方面向服务的架构（SOA）中。能够监视整个系统的服务执行迟早是强制性的。我们的框架允许以安全、有效和自动化的方式创建任何服务输入输出操作的审计跟踪。</p>
<h4 id="toc_51">16.8.5.1. 仅有日志还不够<a class="vnote-anchor" href="#toc_51" data-anchor-icon="#"></a></h4>
<p>  默认情况下，框架将记录任何基于接口的服务进程 ，使用专用的<code>sllServiceCall</code>和<code>sllServiceReturn</code>级日志。您可能会看到类似于以下内容的输出：</p>
<pre><code class="lang-verilog hljs"><span class="hljs-number">18</span>:<span class="hljs-number">03</span>:<span class="hljs-number">18</span> Enter   mORMot<span class="hljs-variable">.TSQLRestServerFullMemory</span>(<span class="hljs-number">024500</span>A0)<span class="hljs-variable">.URI</span>(POST root/DomUserQuery<span class="hljs-variable">.SelectByLogonName</span>/<span class="hljs-number">1</span> inlen=<span class="hljs-number">7</span>)
<span class="hljs-number">18</span>:<span class="hljs-number">03</span>:<span class="hljs-number">18</span> Service call      mORMot<span class="hljs-variable">.TSQLRestServerFullMemory</span>(<span class="hljs-number">024500</span>A0) DomUserQuery<span class="hljs-variable">.SelectByLogonName</span>[<span class="hljs-string">"979"</span>]
<span class="hljs-number">18</span>:<span class="hljs-number">03</span>:<span class="hljs-number">18</span> Server             mORMot<span class="hljs-variable">.TSQLRestServerFullMemory</span>(<span class="hljs-number">024500</span>A0)   POST root/DomUserQuery<span class="hljs-variable">.SelectByLogonName</span> SOA-Interface -&gt; <span class="hljs-number">200</span> <span class="hljs-keyword">with</span> outlen=<span class="hljs-number">21</span> in <span class="hljs-number">16</span> us
<span class="hljs-number">18</span>:<span class="hljs-number">03</span>:<span class="hljs-number">18</span> Service <span class="hljs-keyword">return</span>    mORMot<span class="hljs-variable">.TSQLRestServerFullMemory</span>(<span class="hljs-number">024500</span>A0) {<span class="hljs-string">"result"</span>:[<span class="hljs-number">0</span>],<span class="hljs-string">"id"</span>:<span class="hljs-number">1</span>}
<span class="hljs-number">18</span>:<span class="hljs-number">03</span>:<span class="hljs-number">18</span> Leave   <span class="hljs-number">00</span><span class="hljs-variable">.000</span><span class="hljs-variable">.017</span>
</code></pre>
<p>  上面的行对应<code>dddDomUserCQRS.pas</code>中定义的以下执行方法：</p>
<pre><code class="lang-pascal hljs">  IDomUserQuery = <span class="hljs-keyword">interface</span>(ICQRSService)
    [<span class="hljs-string">'{198C01D6-5189-4B74-AAF4-C322237D7D53}'</span>]
    <span class="hljs-comment">/// will select a single TUser from its logon name</span>
    <span class="hljs-comment">// - then use Get() method to retrieve its content</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SelectByLogonName</span><span class="hljs-params">(<span class="hljs-keyword">const</span> aLogonName: RawUTF8)</span>:</span> TCQRSResult;
  ...
</code></pre>
<p>  实际执行是：</p>
<pre><code class="lang-pascal hljs"> IDomUserQuery.SelectByLogonName(<span class="hljs-string">'979'</span>) -&gt; cqrsSuccess
</code></pre>
<p>  这里<code>cqrsSuccess</code>是枚举结果的第一项，通过方法返回为整数JSON值<code>"result":[0]</code>：</p>
<pre><code class="lang-pascal hljs">  TCQRSResult =
    (cqrsSuccess, cqrsSuccessWithMoreData,
     cqrsUnspecifiedError, cqrsBadRequest, cqrsNotFound,
    ...
</code></pre>
<p>  此详细日志（包括“离开”时的微秒级计时）对于技术支持非常有用，尤其是调查生产服务器上发生的各种错误。 但这对监控更高级别的进程还不够，尤其是在拥有大量并发活动的服务器上（反过来说，提供“过多信息”等于“扼杀信息”）。</p>
<h4 id="toc_52">16.8.5.2. 服务方法跟踪<a class="vnote-anchor" href="#toc_52" data-anchor-icon="#"></a></h4>
<p>  该框架允许选择性地将每个SOA方法执行情况存储在数据库中，包括输入输出参数、精确的时间。</p>
<p>  您可以启用此自动化过程：</p>
<ul>
<li>在服务中，使用<code>TServiceFactoryServer.SetServiceLog()</code>；</li>
<li>或通过<code>TServiceContainerServer.SetServiceLog()</code>开启<code>TSQLRestServer.ServiceContainer</code>实例的所有服务。</li>
</ul>
<p>  例如，您可以为整个REST服务启用它：</p>
<pre><code class="lang-pascal hljs"> (aRestSOAServer.ServiceContainer <span class="hljs-keyword">as</span> TServiceContainerServer).SetServiceLog(
   aRestLogServer,TSQLRecordServiceLog);
</code></pre>
<p>  此单个命令将创建一个审计跟踪，其中<code>aRestSOAServer</code>上的所有服务调用都发送到<code>TSQLRecordServiceLog</code> ORM类的<code>aRestLogServer</code>。 为日志条目保留专用的REST服务将减少主服务的开销，并简化其维护。</p>
<p>  实际存储发生在<code>TSQLRecordServiceLog</code>派生类中：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-title">TSQLRecordServiceLog</span> = <span class="hljs-keyword">class</span>(TSQLRecord)
  ...
  <span class="hljs-keyword">published</span>
    <span class="hljs-comment">/// the 'interface.method' identifier of this call</span>
    <span class="hljs-comment">// - this column will be indexed, for fast SQL queries, with the MicroSec</span>
    <span class="hljs-comment">// column (for performance tuning)</span>
    <span class="hljs-keyword">property</span> Method: RawUTF8 <span class="hljs-keyword">read</span> fMethod <span class="hljs-keyword">write</span> fMethod;
    <span class="hljs-comment">/// the input parameters, as a JSON document</span>
    <span class="hljs-comment">// - will be stored in JSON_OPTIONS_FAST_EXTENDED format, i.e. with</span>
    <span class="hljs-comment">// shortened field names, for smaller TEXT storage</span>
    <span class="hljs-comment">// - content may be searched using JsonGet/JsonHas SQL functions on a</span>
    <span class="hljs-comment">// SQlite3 storage, or with direct document query under MongoDB/PostgreSQL</span>
    <span class="hljs-keyword">property</span> Input: variant <span class="hljs-keyword">read</span> fInput <span class="hljs-keyword">write</span> fInput;
    <span class="hljs-comment">/// the output parameters, as a JSON document, including result: for a function</span>
    <span class="hljs-comment">// - will be stored in JSON_OPTIONS_FAST_EXTENDED format, i.e. with</span>
    <span class="hljs-comment">// shortened field names, for smaller TEXT storage</span>
    <span class="hljs-comment">// - content may be searched using JsonGet/JsonHas SQL functions on a</span>
    <span class="hljs-comment">// SQlite3 storage, or with direct document query under MongoDB/PostgreSQL</span>
    <span class="hljs-keyword">property</span> Output: variant <span class="hljs-keyword">read</span> fOutput <span class="hljs-keyword">write</span> fOutput;
    <span class="hljs-comment">/// the Session ID, if there is any</span>
    <span class="hljs-keyword">property</span> Session: integer <span class="hljs-keyword">read</span> fSession <span class="hljs-keyword">write</span> fSession;
    <span class="hljs-comment">/// the User ID, if there is an identified Session</span>
    <span class="hljs-keyword">property</span> User: integer <span class="hljs-keyword">read</span> fUser <span class="hljs-keyword">write</span> fUser;
    <span class="hljs-comment">/// will be filled by the ORM when this record is written in the database</span>
    <span class="hljs-keyword">property</span> Time: TModTime <span class="hljs-keyword">read</span> fTime <span class="hljs-keyword">write</span> fTime;
    <span class="hljs-comment">/// execution time of this method, in micro seconds</span>
    <span class="hljs-keyword">property</span> MicroSec: integer <span class="hljs-keyword">read</span> fMicroSec <span class="hljs-keyword">write</span> fMicroSec;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  因此，ORM将在其数据库中存储以下表：</p>
<div class="plantuml-diagram"><!--?xml version="1.0" encoding="UTF-8" standalone="no"?--><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentscripttype="application/ecmascript" contentstyletype="text/css" height="235px" preserveAspectRatio="none" style="width:288px;height:235px;" version="1.1" viewBox="0 0 288 235" width="288px" zoomAndPan="magnify" class="view-svg"><defs><filter height="300%" id="fvo3t69uzpqyx" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"></feGaussianBlur><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"></feColorMatrix><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"></feOffset><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"></feBlend></filter></defs><g><ellipse cx="142.1601" cy="115.7281" fill="#FEFECE" filter="url(#fvo3t69uzpqyx)" rx="135.1601" ry="108.7281" style="stroke: #A80036; stroke-width: 1.5;"></ellipse><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="43" x="120.6601" y="27.0286">ID: TID</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="59.8589" x2="224.4613" y1="30.1047" y2="30.1047"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="82" x="101.1601" y="50.1487">Input: variant</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="29.2336" x2="255.0867" y1="57.2248" y2="57.2248"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="117" x="83.6601" y="77.2688">Method: RawUTF8</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="13.7527" x2="270.5676" y1="84.345" y2="84.345"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="110" x="87.1601" y="104.3889">MicroSec: integer</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="8.1039" x2="276.2163" y1="111.4651" y2="111.4651"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="94" x="95.1601" y="131.509">Output: variant</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="11.0204" x2="273.2999" y1="138.5852" y2="138.5852"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="102" x="91.1601" y="158.6291">Session: integer</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="23.1246" x2="261.1956" y1="165.7053" y2="165.7053"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="106" x="89.1601" y="185.7493">Time: TModTime</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="47.8555" x2="236.4648" y1="192.8254" y2="192.8254"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="81" x="101.6601" y="212.8694">User: integer</text><!--MD5=[5cb75a0a273a08ca3293d747f003fabb]
@startuml
usecase UC1 as "ID: TID
- -
Input: variant
- -
Method: RawUTF8
- -
MicroSec: integer
- -
Output: variant
- -
Session: integer
- -
Time: TModTime
- -
User: integer"

@enduml

PlantUML version 1.2019.12(Sun Nov 03 18:24:54 CST 2019)
(GPL source distribution)
Java Runtime: OpenJDK Runtime Environment
JVM: OpenJDK 64-Bit Server VM
Java Version: 13.0.1+9
Operating System: Windows 10
Default Encoding: GBK
Language: zh
Country: CN
--></g></svg></div>
<p>  如您所见，所有输入和输出参数作为两个<code>TDocVariant</code>实例都是记录的一部分。由于它们存储为JSON/TEXT，您可以直接对其内容执行某些请求，尤其是实际存储在MongoDB数据库中时：您甚至可以对参数值使用专用索引，运行高级map/reduce查询。您可以使用<code>TInterfaceFactory.SetOptions()</code>设置<code>optNoLogInput</code>或<code>optNoLogOutput</code>来隐藏所有输入或输出参数值，或使用<code>TInterfaceFactory.RegisterUnsafeSPIType</code>将某些值类型定义为包含敏感个人信息（SPI）。</p>
<p>  由于非常精确的计时（微秒级）是信息的一部分，因此您可以使用简单的SQL子句进行过滤或高级统计。SOA系统监控及识别潜在问题从未如此简单。如您可以轻松地从数据库中提取这些信息，并提供实时可视化监控图表。或者识别和监视不寻常的执行模式（如意外的时间或错误代码），针对这些问题可以使用一些SQL请求：这些SQL语句可以定期自动运行，以预防实际问题的发生。</p>
<h4 id="toc_53">16.8.5.3. 异步外部调用跟踪<a class="vnote-anchor" href="#toc_53" data-anchor-icon="#"></a></h4>
<p>  有时，您的服务端可能是另一个进程的客户端。在SOA环境中，您可以与外部流程的第三方REST服务进行交互，如发送实时通知。</p>
<p>  在REST客户端实例上，您可以对给定服务执行<code>TServiceFactoryClient.SendNotifications()</code>方法：</p>
<pre><code class="lang-pascal hljs">aNotificationClientService.SendNotifications(aServicesLogRest,
    TSQLRecordServiceNotifications, fSettings.NotificationsRetrySeconds);
</code></pre>
<p>  此单个命令将在<code>TSQLRecordServiceNotifications</code> ORM类的<code>aServicesLogRest</code>中创建一个审计跟踪，其中所有通知调用都发送到<code>aNotificationClientService</code>。</p>
<p>  您可以使用以下<code>TSQLRecordServiceNotifications</code>类：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-title">TSQLRecordServiceNotifications</span> = <span class="hljs-keyword">class</span>(TSQLRecordServiceLog)
  ...
  <span class="hljs-keyword">published</span>
    <span class="hljs-comment">/// when this notification has been sent</span>
    <span class="hljs-comment">// - equals 0 until it was actually notified</span>
    <span class="hljs-keyword">property</span> Sent: TTimeLog <span class="hljs-keyword">read</span> fSent <span class="hljs-keyword">write</span> fSent;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  将存储在下表中：</p>
<div class="plantuml-diagram"><!--?xml version="1.0" encoding="UTF-8" standalone="no"?--><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentscripttype="application/ecmascript" contentstyletype="text/css" height="261px" preserveAspectRatio="none" style="width:321px;height:261px;" version="1.1" viewBox="0 0 321 261" width="321px" zoomAndPan="magnify" class="view-svg"><defs><filter height="300%" id="f1l9dr1zbkj9yq" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"></feGaussianBlur><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"></feColorMatrix><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"></feOffset><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"></feBlend></filter></defs><g><ellipse cx="158.6441" cy="128.9153" fill="#FEFECE" filter="url(#f1l9dr1zbkj9yq)" rx="151.6441" ry="121.9153" style="stroke: #A80036; stroke-width: 1.5;"></ellipse><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="43" x="137.1441" y="26.8706">ID: TID</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="71.0897" x2="246.1985" y1="29.9468" y2="29.9468"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="101" x="108.1441" y="49.9907">Sent: TTimeLog</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="37.132" x2="280.1562" y1="57.0669" y2="57.0669"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="82" x="117.6441" y="77.1108">Input: variant</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="18.5744" x2="298.7138" y1="84.187" y2="84.187"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="117" x="100.1441" y="104.231">Method: RawUTF8</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="9.59" x2="307.6982" y1="111.3071" y2="111.3071"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="110" x="103.6441" y="131.3511">MicroSec: integer</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="8.4623" x2="308.8259" y1="138.4272" y2="138.4272"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="94" x="111.6441" y="158.4712">Output: variant</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="15.0074" x2="302.2808" y1="165.5474" y2="165.5474"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="102" x="107.6441" y="185.5913">Session: integer</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="30.3856" x2="286.9025" y1="192.6675" y2="192.6675"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="106" x="105.6441" y="212.7114">Time: TModTime</text><line style="stroke: #A80036; stroke-width: 1.0;" x1="58.5509" x2="258.7373" y1="219.7876" y2="219.7876"></line><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacingAndGlyphs" textLength="81" x="118.1441" y="239.8315">User: integer</text><!--MD5=[60f00c01035cc7cff2a3839b1e37b66e]
@startuml
usecase UC1 as "ID: TID
- -
Sent: TTimeLog
- -
Input: variant
- -
Method: RawUTF8
- -
MicroSec: integer
- -
Output: variant
- -
Session: integer
- -
Time: TModTime
- -
User: integer"

@enduml

PlantUML version 1.2019.12(Sun Nov 03 18:24:54 CST 2019)
(GPL source distribution)
Java Runtime: OpenJDK Runtime Environment
JVM: OpenJDK 64-Bit Server VM
Java Version: 13.0.1+9
Operating System: Windows 10
Default Encoding: GBK
Language: zh
Country: CN
--></g></svg></div>
<p>  附加的<code>Sent</code>属性将包含发送通知的<code>TTimeLog</code>时间戳。</p>
<p>  实际上，通过此通知服务执行的所有方法将首先存储在此表中，然后远程HTTP通知将在后台异步进行。 传输将按顺序（先进先出），如果出现任何连接问题（如远程服务器未返回<code>200 HTTP SUCCESS</code>状态码），它将不会移动到下一个条目，并将在提供给<code>SendNotifications()</code>方法的<code>NotificationsRetrySeconds</code>计时周期后重试。</p>
<p>  当然，您可以定义自己的子类，以自定义目标审计跟踪表：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TSQLMyNotifications</span> = <span class="hljs-keyword">class</span>(TSQLRecordServiceNotifications);
</code></pre>
<p>  感谢那些<code>TSQLRecordServiceLog</code>类，高级支持和分析从未变得如此容易。 这些特性的实际实现已经优化， 如通过<a href="">批次序列添加/更新/删除记录</a>延迟写入操作，或者用于异步通知处理的专用后台线程，以最小化对主要性能的影响。</p>
<h3 id="toc_54">16.8.6. 传输内容<a class="vnote-anchor" href="#toc_54" data-anchor-icon="#"></a></h3>
<p>  根据请求的URI，所有数据都作为JSON数组或对象传输。</p>
<p>  我们将讨论如何在应用程序中传输数据。</p>
<h4 id="toc_55">16.8.6.1. 请求格式<a class="vnote-anchor" href="#toc_55" data-anchor-icon="#"></a></h4>
<p>  如上所述，有几种可用的路由模式，由给定的<code>TSQLRestServerURIContext</code>派生类定义：</p>
<div class="mermaid-diagram"><svg id="mermaid-diagram-4" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 500 198" style="max-width:500px;"><style type="text/css" title="mermaid-svg-internal-css">/*  */
#mermaid-diagram-4 .node&gt;rect { ; }
#mermaid-diagram-4 .node text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-4 .edgeLabel text  { fill:#000; stroke:none; font-weight:300; font-family:"Helvetica Neue",Helvetica,Arial,sans-serf; font-size:14px; }
#mermaid-diagram-4 .cluster rect  { rx:4px; fill: rgb(255, 255, 222); rx: 4px; stroke: rgb(170, 170, 51); stroke-width: 1px; }
#mermaid-diagram-4 .cyan&gt;rect, .cyan&gt;polygon, .cyan&gt;circle, .cyan&gt;ellipse { fill:#9ff;  stroke:#fff; }
/*  */
</style><g><g class="output"><g class="clusters"></g><g class="edgePaths"><g class="edgePath" style="opacity: 1;"><path class="path" d="M127,64L127,89L192.1595744680851,114" marker-end="url(#arrowhead58)" style="fill:none"></path><defs><marker id="arrowhead58" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g><g class="edgePath" style="opacity: 1;"><path class="path" d="M372,64L372,89L306.8404255319149,114" marker-end="url(#arrowhead59)" style="fill:none"></path><defs><marker id="arrowhead59" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="8" markerHeight="6" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowheadPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"></path></marker></defs></g></g><g class="edgeLabels"><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="" style="opacity: 1;"><g transform="translate(0,0)" class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"></span></div></foreignObject></g></g></g><g class="nodes"><g class="node cyan" id="A" transform="translate(127,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-107" y="-22" width="214" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-97,-12)"><foreignObject width="194" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSQLRestRoutingJSON_RPC</div></foreignObject></g></g></g><g class="node cyan" id="B" transform="translate(249.5,136)" style="opacity: 1;"><rect rx="0" ry="0" x="-105" y="-22" width="210" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-95,-12)"><foreignObject width="190" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSQLRestServerURIContext</div></foreignObject></g></g></g><g class="node cyan" id="C" transform="translate(372,42)" style="opacity: 1;"><rect rx="0" ry="0" x="-88" y="-22" width="176" height="44"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-78,-12)"><foreignObject width="156" height="24"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml">TSQLRestRoutingREST</div></foreignObject></g></g></g></g></g></g></svg></div>
<p>  相应的描述可能是：</p>
<table>
<thead>
<tr>
<th></th>
<th>TSQLRestRoutingREST</th>
<th>TSQLRestRoutingJSON_RPC</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mode</td>
<td>RESTful</td>
<td>JSON-RPC</td>
</tr>
<tr>
<td>Default</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>URI scheme</td>
<td>/Model/Interface.Method[/ClientDrivenID]<br>或/Model/Interface/Method[/ClientDrivenID]<br>+可选的uri编码参数</td>
<td>/Model/Interface</td>
</tr>
<tr>
<td>Body content</td>
<td>JSON参数数组<br>如果参数在URI上编码，则为空</td>
<td>{<code>"method":"*MethodName*","params":[...][,"id":*ClientDrivenID*]}</code></td>
</tr>
<tr>
<td>Body content（替代）</td>
<td>JSON参数对象<br>如果参数在URI上编码，则为空</td>
<td>{<code>"method":"*MethodName*","params":...[,"id":*ClientDrivenID*]}</code></td>
</tr>
<tr>
<td>Security</td>
<td>每种方法的RESTful身份验证<br>或用于整个服务(接口)</td>
<td>整个服务(接口)的RESTful身份验证</td>
</tr>
<tr>
<td>Speed</td>
<td>快10%</td>
<td>慢10%</td>
</tr>
</tbody>
</table>
<p>  大多数情况下，输入参数将按照<code>const/var</code>方法参数的准确顺序作为JSON数组值传输。</p>
<p>  另外，也接受带参数名称的JSON对象。 这将比JSON参数数组略慢，但可能很方便，具体取决于客户端。</p>
<p>  最后但也最重要的是，<code>TSQLRestRoutingREST</code>能够解码URI编码参数，与大多数之前普通常的HTTP请求一样。</p>
<p>  要使用的路由在<code>TSQLRest.ServiceRouting</code>全局属性中定义，当然客户端和服务端应该相匹配。 按照设计，您永远不要将抽象的<code>TSQLRestServerURIContext</code>分配给该属性。</p>
<p>  <code>TSQLRestServerURIContext</code>抽象类定义了以下方法，这些方法将被继承的实现覆盖，以反映RESTful路由和传输等各个方面希望的行为：</p>
<pre><code class="lang-pascal hljs">  TSQLRestServerURIContext = <span class="hljs-keyword">class</span>
  <span class="hljs-keyword">protected</span>
 ...
    <span class="hljs-comment">/// retrieve RESTful URI routing</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">URIDecodeREST</span>:</span> boolean; <span class="hljs-keyword">virtual</span>;
    <span class="hljs-comment">/// retrieve method-based SOA URI routing with optional RESTful mode</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">URIDecodeSOAByMethod</span>;</span> <span class="hljs-keyword">virtual</span>;
    <span class="hljs-comment">/// retrieve interface-based SOA</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">URIDecodeSOAByInterface</span>;</span> <span class="hljs-keyword">virtual</span>; <span class="hljs-keyword">abstract</span>;
    <span class="hljs-comment">/// process authentication</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Authenticate</span>:</span> boolean; <span class="hljs-keyword">virtual</span>;
    <span class="hljs-comment">/// direct launch of a method-based service</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ExecuteSOAByMethod</span>;</span> <span class="hljs-keyword">virtual</span>;
    <span class="hljs-comment">/// direct launch of an interface-based service</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ExecuteSOAByInterface</span>;</span> <span class="hljs-keyword">virtual</span>; <span class="hljs-keyword">abstract</span>;
    <span class="hljs-comment">/// handle GET/LOCK/UNLOCK/STATE verbs for ORM/CRUD process</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ExecuteORMGet</span>;</span> <span class="hljs-keyword">virtual</span>;
    <span class="hljs-comment">/// handle POST/PUT/DELETE/BEGIN/END/ABORT verbs for ORM/CRUD process</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">ExecuteORMWrite</span>;</span> <span class="hljs-keyword">virtual</span>;
 ...
</code></pre>
<p>  大多数情况下，提供的<code>TSQLRestRoutingREST</code>和<code>TSQLRestRoutingJSON_RPC</code>类能够满足您的要求。</p>
<h5 id="toc_56">16.8.6.1.1. REST模式<a class="vnote-anchor" href="#toc_56" data-anchor-icon="#"></a></h5>
<h6 id="toc_57">16.8.6.1.1.1. JSON数组参数传输<a class="vnote-anchor" href="#toc_57" data-anchor-icon="#"></a></h6>
<p>  在默认的<code>TSQLRestRoutingREST</code>模式中，在URI内标识服务和操作（即接口和方法）。 消息体是所提供参数的标准JSON数组（即所有<code>const</code>和<code>var</code>参数）。</p>
<p>  以下是<code>ICalculator.Add</code>的典型请求：</p>
<pre><code class="hljs"> POST /root/Calculator.Add
 (...)
 [1,2]
</code></pre>
<p>  这里我们使用POST动作，但如果需要，框架还允许其他方法，如GET（来自常规浏览器）。 纯Delphi客户端实现将仅使用POST。</p>
<p>  对于<code>sicClientDriven</code>模式服务，所需的实例<code>ID</code>将附加到URI：</p>
<pre><code class="lang-pascal hljs"> POST /root/ComplexNumber.Add/<span class="hljs-number">1234</span>
 (...)
 [<span class="hljs-number">20</span>,<span class="hljs-number">30</span>]
</code></pre>
<p>  这里，<code>1234</code>是服务端实例的<code>ID</code>标识符，用于在<code>sicClientDriven</code>模式下跟踪实例生命周期。 在URI中传输客户端会话ID的一个好处是，它在我们的RESTful身份验证方案中会更安全 ：每个方法（甚至任何客户端驱动的会话ID）都将正确签名。</p>
<h6 id="toc_58">16.8.6.1.1.2. JSON对象参数传输<a class="vnote-anchor" href="#toc_58" data-anchor-icon="#"></a></h6>
<p>  mORMot服务端输入参数还接受编码为名值对的JSON对象，而不仅是JSON数组：</p>
<pre><code class="hljs"> POST /root/Calculator.Add
 (...)
 {"n1":1,"n2":2}
</code></pre>
<p>  当然，值的顺序在JSON对象中不是强制的，因为参数将按名称查找。 因此，以下请求将与前一个请求相同：</p>
<pre><code class="hljs"> POST /root/Calculator.Add
 (...)
 {"n2":2,"n1":1}
</code></pre>
<p>  对于<code>sicClientDriven</code>模式服务，所需的实例<code>ID</code>将附加到URI：</p>
<pre><code class="hljs"> POST /root/ComplexNumber.Add/1234
 (...)
 {"aReal":20,"aImaginary":30}
</code></pre>
<p>  在某些情况下，在客户端命名参数可能很有用。 但这不应该是默认值，因为它会稍微慢一点（用于解析和检查名称），使用更多传输带宽。</p>
<p>  输入JSON对象中缺少的任何参数都将替换为默认值。 如以下将运行<code>IComplexNumber.Add(0,2)</code>：</p>
<pre><code class="hljs"> POST /root/Calculator.Add
 (...)
 {"n2":2}
</code></pre>
<p>  输入的JSON对象中的任何未知参数都将被忽略。 如果您想传输一些通用执行上下文（例如MVC模型中的全局“数据范围”），并让服务仅使用它所需的值，那么这可能很方便。</p>
<pre><code class="hljs"> POST /root/ComplexNumber.Add/1234
 (...)
 {"Session":"1234","aImaginary":30,"aReal":20,"UserLogged":"Nikita"}
</code></pre>
<p>  当然，额外的值将消耗一些带宽，但服器端的处理成本可以忽略不计，因为我们的实现将忽略这些意外的属性，而不为它们分配任何内存。</p>
<h6 id="toc_59">16.8.6.1.1.3. URI级参数编码<a class="vnote-anchor" href="#toc_59" data-anchor-icon="#"></a></h6>
<p>  在<code>TSQLRestRoutingREST</code>模式中，如果消息体为空，服务端还能够从URI检索参数。 这不是用于Delphi客户端的（因为这会更复杂、更慢），但如果需要，它可以用于其它客户端：</p>
<pre><code class="hljs"> POST root/Calculator.Add?+%5B+1%2C2+%5D
 GET root/Calculator.Add?+%5B+1%2C2+%5D
</code></pre>
<p>  在上面的代码行中，<code>+%5B+1%2C2+%5D</code>将在服务器端解码为[1,2]。 结合GET使用，它可能更适合远程AJAX连接。</p>
<p>  作为替代方案，您可以以常规HTML方式在URI级别对参数进行编码和命名：</p>
<pre><code class="hljs"> GET root/Calculator.Add?n1=1&amp;n2=2
</code></pre>
<p>  由于参数是命名的，因此它们可以按任意顺序排列。 如果缺少任何参数，它将被其默认值替换（例如<code>0</code>表示数字或<code>''</code>表示字符串）。</p>
<p>  对于从任意类型的客户端来使用服务，这可能非常简单方便。</p>
<p>  请注意，使用URI通过HTTP传递某些数据时存在已知的大小限制。 RFC 2616官方标准建议将URI大小限制为255个字符，而在实践中，在URI中传输最多2048个字符看起来是安全的。 如果您想摆脱这种限制，只需使用默认传输的JSON数组作为请求体。</p>
<p>  作为替代方案，URI可以写为<code>/RootName/InterfaceName/MethodName</code>。 它可能更符合RESTful，具体取决于您的客户端策略。 因此，以下URI将等同于先前的请求：</p>
<pre><code class="hljs"> POST /root/Calculator/Add
 POST /root/ComplexNumber/Add/1234
 POST root/Calculator/Add?+%5B+1%2C2+%5D
 GET root/Calculator/Add?+%5B+1%2C2+%5D
 GET root/Calculator/Add?n1=1&amp;n2=2
</code></pre>
<p>  Delphi客户端，将始终使用<code>/RootName/InterfaceName.MethodName</code>方案。</p>
<h6 id="toc_60">16.8.6.1.1.4. JSON对象传输<a class="vnote-anchor" href="#toc_60" data-anchor-icon="#"></a></h6>
<p>  默认情况下，mORMot客户端将发送所有值，作为JSON数组传输，没有参数名称，如下所见：</p>
<pre><code class="hljs"> POST /root/Calculator.Add
 (...)
 [1,2]
</code></pre>
<p>  但是如果<code>TServiceFactoryClient.ParamsAsJSONObject</code>设置为<code>true</code>，则客户端传输的值将被编码为JSON对象：</p>
<pre><code class="hljs"> POST /root/Calculator.Add
 (...)
 {"n1":1,"n2":2}
</code></pre>
<p>  这可能有助于将一些值以另一种格式传输到给定服务的非mORMot服务器。</p>
<h6 id="toc_61">16.8.6.1.1.5. 原始二进制传输<a class="vnote-anchor" href="#toc_61" data-anchor-icon="#"></a></h6>
<p>  如果您的目的是上传一些二进制数据，<code>RawByteString</code>和<code>TSQLRawBlob</code>输入参数默认将作为Base64编码的JSON文本传输。</p>
<p>  您可以定义<a href="">基于方法的客户端-服务器服务</a>传输原始二进制数据，而无需Base64编码开销。它允许访问底层输入内容类型和编码，即使从HTTP上传多部分文件。</p>
<p>  作为替代方案，如果您使用默认的<code>TSQLRestRoutingREST</code>路由，并定义单个<code>RawByteString</code>或<code>TSQLRawBlob</code>输入参数，它将作为原始POST处理，使用二进制主体，并定义<code>'application/octet-stream'</code>mime类型。这可能更适合通过Internet进行远程访问。</p>
<h5 id="toc_62">16.8.6.1.2. JSON-RPC<a class="vnote-anchor" href="#toc_62" data-anchor-icon="#"></a></h5>
<h6 id="toc_63">16.8.6.1.2.1. JSON数组参数传输<a class="vnote-anchor" href="#toc_63" data-anchor-icon="#"></a></h6>
<p>  如果使用<code>TSQLRestRoutingJSON_RPC</code>模式，则URI将定义接口，通过使用方法名称及参数，例如：</p>
<pre><code class="hljs"> POST /root/Calculator
 (...)
 {"method":"Add","params":[1,2],"id":0}
</code></pre>
<p>  这里，<code>"id"</code>字段不能设置（可能不存在），因为它在<code>sicShared</code>模式下没有使用。</p>
<p>  对于<code>sicClientDriven</code>模式服务：</p>
<pre><code class="hljs"> POST /root/ComplexNumber
 (...)
 {"method":"Add","params":[20,30],"id":1234}
</code></pre>
<h6 id="toc_64">16.8.6.1.2.2. JSON对象参数传输<a class="vnote-anchor" href="#toc_64" data-anchor-icon="#"></a></h6>
<p>  作为替代方法，您可以将值作为包含命名参数值的JSON对象传输，而不是JSON数组：</p>
<pre><code class="hljs"> POST /root/Calculator
 (...)
 {"method":"Add","params":{"n1":1,"n2":2},"id":0}
</code></pre>
<p>  这里，相同的规则适用于<code>TSQLRestRoutingREST</code>模式：</p>
<ul>
<li>任何缺失的参数将替换为默认值；</li>
<li>属性顺序不再敏感；</li>
<li>不需要的参数将被忽略。</li>
</ul>
<p>  根据定义，<code>TSQLRestRoutingJSON_RPC</code>模式无法处理URI编码的参数。实际上，JSON-RPC模式要求URI仅用于标识服务，并将整个执行上下文作为主体内容传输。</p>
<h5 id="toc_65">16.8.6.1.3. REST还是JSON-RPC模式？<a class="vnote-anchor" href="#toc_65" data-anchor-icon="#"></a></h5>
<p>  对于标准的mORMot Delphi客户端或任何支持的跨平台客户端 ，首选<code>TSQLRestRoutingREST</code>，即使对于Smart Mobile Studio，提供的库也完全实现了该路由方案，它是更快、更安全和最模块化的模式。</p>
<p>  实际上，我们发现<code>TSQLRestRoutingJSON_RPC</code>模式有点慢。由于方法名称是URI的一部分，因此签名将比JSON-RPC模式更大，因此它也更安全。其检索URI编码参数的能力也是有用的，如对于服务端，除了SOA端点之外，还需要一些动态HTML页面，并具有适当的HTTP缓存能力。</p>
<p>  当然，可以使用<code>TSQLRestRoutingJSON_RPC</code>模式作为替代，取决于客户需求和技术限制，比如您的客户希望JSON-RPC兼容的通信。</p>
<p>  您可以根据需要选择正确的路由方案。</p>
<h4 id="toc_66">16.8.6.2. 响应格式<a class="vnote-anchor" href="#toc_66" data-anchor-icon="#"></a></h4>
<h5 id="toc_67">16.8.6.2.1. JSON对象标准响应<a class="vnote-anchor" href="#toc_67" data-anchor-icon="#"></a></h5>
<h6 id="toc_68">16.8.6.2.1.1. JSON响应<a class="vnote-anchor" href="#toc_68" data-anchor-icon="#"></a></h6>
<p>####### 16.8.6.2.1.1.1. 返回JSON数组</p>
<p>  无论使用何种路由模式，框架都将始终以相同的格式返回数据。</p>
<p>  本质上，这是一个JSON对象，嵌套在<code>"result":</code>属性中，以及客户端标识的<code>"id":</code>值（如在<code>sicShared</code>模式下总是0）：</p>
<pre><code class="hljs"> POST /root/Calculator.Add
 (...)
 [1,2]
</code></pre>
<p>  响应如下：</p>
<pre><code class="lang-json hljs"> {<span class="hljs-attr">"result"</span>:[<span class="hljs-number">3</span>]}
</code></pre>
<p>  对于<code>sicClientDriven</code>、<code>sicPerSession</code>、<code>sicPerUser</code>、<code>sicPerGroup</code>或<code>sicPerThread</code>模式服务，响应将包含附加的<code>"id":...</code>成员，用于标识相应的会话：</p>
<pre><code class="lang-json hljs"> {<span class="hljs-attr">"result"</span>:[<span class="hljs-number">3</span>],<span class="hljs-attr">"id"</span>:<span class="hljs-number">1234</span>}
</code></pre>
<p>  在<code>sicSingle</code>和<code>sicShared</code>模式中，不会传输<code>"id":0</code>成员。</p>
<p>  结果JSON数组包含所有<code>var</code>和<code>out</code>参数值（按其声明顺序），然后是方法的结果。</p>
<p>  例如，以下是<code>TSQLRestRoutingREST</code>模式下<code>ICalculator.ComplexCall</code>请求的传输流：</p>
<pre><code class="hljs"> POST root/Calculator.ComplexCall
 (...)
 [[288722014,1231886296], ["one","two","three"], ["ABC","DEF","GHIJK"], "ï¿BgAAAAAAAAAAAAAAAAAAACNEOlxEZXZcbGliXFNRTGl0ZTNcZXhlXFRlc3RTUUwzLmV4ZQ==", "ï¿Xow1EdkXbUkDYWJj"]
</code></pre>
<p>  将会这样响应：</p>
<pre><code class="lang-json hljs"> '{<span class="hljs-attr">"result"</span>:[ [<span class="hljs-string">"ABC"</span>,<span class="hljs-string">"DEF"</span>,<span class="hljs-string">"GHIJK"</span>,<span class="hljs-string">"one,two,three"</span>], <span class="hljs-string">"ï¿X4w1EdgXbUkUMjg4NzIyMDE0LDEyMzE4ODYyOTY="</span>, <span class="hljs-string">"ï¿Xow1EdkXbUkjRDpcRGV2XGxpYlxTUUxpdGUzXGV4ZVxUZXN0U1FMMy5leGU="</span>]}'
</code></pre>
<p>  它与方法的<code>var/const/out</code>参数声明是相符的：</p>
<pre><code class="lang-pascal hljs"> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ComplexCall</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Ints: TIntegerDynArray; Strs1: TRawUTF8DynArray;
   <span class="hljs-keyword">var</span> Str2: TWideStringDynArray; <span class="hljs-keyword">const</span> Rec1: TVirtualTableModuleProperties;
   <span class="hljs-keyword">var</span> Rec2: TSQLRestCacheEntryValue)</span>:</span> TSQLRestCacheEntryValue;
</code></pre>
<p>  它的实现：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceCalculator</span>.<span class="hljs-title">ComplexCall</span><span class="hljs-params">(<span class="hljs-keyword">const</span> Ints: TIntegerDynArray;
  Strs1: TRawUTF8DynArray; <span class="hljs-keyword">var</span> Str2: TWideStringDynArray; <span class="hljs-keyword">const</span> Rec1: TVirtualTableModuleProperties;
  <span class="hljs-keyword">var</span> Rec2: TSQLRestCacheEntryValue)</span>:</span> TSQLRestCacheEntryValue;
<span class="hljs-keyword">var</span> i: integer;
<span class="hljs-keyword">begin</span>
  result := Rec2;
  result.JSON := StringToUTF8(Rec1.FileExtension);
  i := length(Str2);
  SetLength(Str2,i+<span class="hljs-number">1</span>);
  Str2[i] := UTF8ToWideString(RawUTF8ArrayToCSV(Strs1));
  inc(Rec2.ID);
  dec(Rec2.Timestamp);
  Rec2.JSON := IntegerDynArrayToCSV(Ints,length(Ints));
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  请注意，<code>TIntegerDynArray</code>、<code>TRawUTF8DynArray</code>和<code>TWideStringDynArray</code>值被编码为JSON数组，而复杂记录（如<code>TSQLRestCacheEntryValue</code>）则进行Base64编码。</p>
<p>  如果要传输一些二进制blob内容，请考虑使用<code>RawByteString</code>类型的参数，该参数将在线路上传输Base64编码的JSON文本。</p>
<p>  框架能够处理类实例参数，如以下接口，拥有发布属性的<code>TPersistent</code>派生类（与<code>TSQLRecord</code> ORM实例相同）：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  <span class="hljs-title">TComplexNumber</span> = <span class="hljs-keyword">class</span>(TPersistent)
  <span class="hljs-keyword">private</span>
    fReal: Double;
    fImaginary: Double;
  <span class="hljs-keyword">public</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span> <span class="hljs-title">Create</span><span class="hljs-params">(aReal, aImaginary: double)</span>;</span> <span class="hljs-keyword">reintroduce</span>;
  <span class="hljs-keyword">published</span>
    <span class="hljs-keyword">property</span> Real: Double <span class="hljs-keyword">read</span> fReal <span class="hljs-keyword">write</span> fReal;
    <span class="hljs-keyword">property</span> Imaginary: Double <span class="hljs-keyword">read</span> fImaginary <span class="hljs-keyword">write</span> fImaginary;
  <span class="hljs-keyword">end</span>;

  IComplexCalculator = <span class="hljs-keyword">interface</span>(ICalculator)
    [<span class="hljs-string">'{8D0F3839-056B-4488-A616-986CF8D4DEB7}'</span>]
    <span class="hljs-comment">/// purpose of this unique method is to substract two complex numbers</span>
    <span class="hljs-comment">// - using class instances as parameters</span>
    <span class="hljs-function"><span class="hljs-keyword">procedure</span> <span class="hljs-title">Substract</span><span class="hljs-params">(n1,n2: TComplexNumber; <span class="hljs-keyword">out</span> Result: TComplexNumber)</span>;</span>
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  如上所述，由于函数不允许返回一个类，所以在这个方法声明中，结果被声明为<code>out</code>参数。</p>
<p>  在传输过程中，<code>TComplexNumber</code>参数的发布属性将被序列化为<code>"result":[...]</code>JSON数组中的标准JSON对象：</p>
<pre><code class="hljs"> POST root/ComplexCalculator.Substract
 (...)
 [{"Real":2,"Imaginary":3},{"Real":20,"Imaginary":30}]
</code></pre>
<p>  将会这样响应：</p>
<pre><code class="lang-json hljs"> {<span class="hljs-attr">"result"</span>:[{<span class="hljs-attr">"Real"</span>:<span class="hljs-number">-18</span>,<span class="hljs-attr">"Imaginary"</span>:<span class="hljs-number">-27</span>}]}
</code></pre>
<p>####### 16.8.6.2.1.1.2. 返回JSON对象</p>
<p>  如果<code>TServiceFactoryServer.ResultAsJSONObject</code>设置为<code>true</code>，则不会使用<code>"result":[...]</code>JSON数组输出值，而是通过<code>"result":{... }</code>JSON对象，使用<code>var/out</code>参数名称作为对象字段，<code>"Result":</code>用于函数结果：</p>
<pre><code class="lang-json hljs"> {<span class="hljs-attr">"result"</span>:{<span class="hljs-attr">"Result"</span>:{<span class="hljs-attr">"Real"</span>:<span class="hljs-number">-18</span>,<span class="hljs-attr">"Imaginary"</span>:<span class="hljs-number">-27</span>}}}
</code></pre>
<p>  <code>TServiceFactoryServer.ResultAsJSONObjectWithoutResult</code>属性可用于去掉主<code>"Result":</code>对象。</p>
<p>  默认返回如下JSON数组内容：</p>
<pre><code class="hljs">GET root/Calculator/Add?n1=1&amp;n2=2
 ...
{"result":[3]}
</code></pre>
<p>  如果<code>TServiceFactoryServer.ResultAsJSONObject</code>为<code>true</code>，则返回以下JSON：</p>
<pre><code class="hljs">GET root/Calculator/Add?n1=1&amp;n2=2
 ...
{"result":{"Result":3}}
</code></pre>
<p>  或者，如果<code>TServiceFactoryServer.ResultAsJSONObjectWithoutResult</code>为<code>true</code>：</p>
<pre><code class="hljs">GET root/Calculator/Add?n1=1&amp;n2=2
 ...
{"Result":3}
</code></pre>
<p>  所有这些JSON数组或对象内容都完全符合标准JSON声明，可以由任何AJAX客户端直接生成和使用。 <code>TServiceFactoryServer.ResultAsJSONObject</code>选项使得更容易使用mORMot服务，因为所有输出值都将在<code>"result":</code>JSON对象中命名。</p>
<h6 id="toc_69">16.8.6.2.1.2. 返回原始JSON内容<a class="vnote-anchor" href="#toc_69" data-anchor-icon="#"></a></h6>
<p>  默认情况下，如果要使用基于接口的服务传输JSON内容，并使用<code>RawUTF8</code>将其转换为JSON字符串。 因此，任何JSON特殊字符（如<code>"</code>或<code>\</code>或<code>[</code>）都将被转义。这将减慢服务端和客户端的进程，并增加传输带宽。</p>
<p>  例如，如果您定义了如下的方法：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceRemoteSQL</span>.<span class="hljs-title">Execute</span><span class="hljs-params">(<span class="hljs-keyword">const</span> aSQL: RawUTF8; aExpectResults, aExpanded: Boolean)</span>:</span> RawUTF8;
<span class="hljs-keyword">var</span> res: ISQLDBRows;
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">if</span> fProps=<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span>
    <span class="hljs-keyword">raise</span> Exception.Create(<span class="hljs-string">'Connect call required before Execute'</span>);
  res := fProps.ExecuteInlined(aSQL,aExpectResults);
  <span class="hljs-keyword">if</span> res=<span class="hljs-keyword">nil</span> <span class="hljs-keyword">then</span>
    result := <span class="hljs-string">''</span> <span class="hljs-keyword">else</span>
    result := res.FetchAllAsJSON(aExpanded);
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  <code>FetchAllAsJSON()</code>方法将返回JSON数组内容，但在通过<code>RawUTF8</code>变量传输时将作为JSON字符串进行转义。</p>
<p>  定义了专用<code>RawJSON</code>类型，用于向mORMot核心指明该UTF-8文本是有效的JSON内容，不需要对其进行转义。</p>
<p>  也就是说，定义如下方法将提高处理速度并减少使用带宽：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceRemoteSQL</span>.<span class="hljs-title">Execute</span><span class="hljs-params">(<span class="hljs-keyword">const</span> aSQL: RawUTF8; aExpectResults, aExpanded: Boolean)</span>:</span> RawJSON;
</code></pre>
<p>  有关使用此功能的一些工作代码，请参阅示例<code>"16 - Execute SQL via services"</code>。</p>
<p>  因此，使用<code>RawJSON</code>也会使传输的内容更加AJAX友好，因为返回的值将是有效的JSON数组或对象，而不是需要JavaScript“unstringification”的JSON字符串。</p>
<h6 id="toc_70">16.8.6.2.1.3. 返回错误<a class="vnote-anchor" href="#toc_70" data-anchor-icon="#"></a></h6>
<p>  如果出现错误，将返回标准消息对象：</p>
<pre><code class="lang-json hljs">{
 <span class="hljs-attr">"ErrorCode"</span>:<span class="hljs-number">400</span>,
 <span class="hljs-attr">"ErrorText"</span>:<span class="hljs-string">"Error description"</span>
}
</code></pre>
<p>  服务端服务实现可返回下列错误描述：</p>
<table>
<thead>
<tr>
<th>错误提示</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>Method name required</td>
<td>TSQLRestRoutingJSON_RPC调用缺少<code>"method":</code>字段</td>
</tr>
<tr>
<td>Unknown method</td>
<td>方法名无效的TSQLRestRoutingJSON_RPC调用（在这种模式下，没有特定的消息，因为JSON响应可能是有效请求）</td>
</tr>
<tr>
<td>Parameters required</td>
<td>服务器期望至少有一个JSON空数组（如<code>[]</code>）作为参数</td>
</tr>
<tr>
<td>Unauthorized method</td>
<td>当前经过身份验证的用户组不允许使用此方法，请参阅上面的安全性</td>
</tr>
<tr>
<td>Not allowed to publish signature</td>
<td>客户端请求了接口签名，但服务器策略上不允许这样做（请参阅TServiceContainerServer.PublishSignature）</td>
</tr>
<tr>
<td>... instance id:? not found or deprecated</td>
<td>提供的<code>“id”:</code>参数指向错误的实例（在<code>sicPerSession / sicPerUser / sicPerGroup</code>模式下）</td>
</tr>
<tr>
<td>ExceptionClass: 异常消息( <code>500 Internal Server Error</code>)</td>
<td>方法执行期间引发异常</td>
</tr>
</tbody>
</table>
<p>  在客户端，您可能会遇到以下<code>EInterfaceFactoryException</code>消息，通常开头为<code>'Invalid fake IInterfaceName.MethodName interface call'</code>文本：</p>
<table>
<thead>
<tr>
<th>错误提示</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>unexpected self</td>
<td><code>self</code>确实作为底层实现细节存在，但不能被传输</td>
</tr>
<tr>
<td>JSON array/object result expected</td>
<td>从服务器返回的内容既不是JSON数组，也不是JSON对象</td>
</tr>
<tr>
<td>unexpected parameter "...."</td>
<td>服务器返回一个JSON对象，该对象的成员名未知或无效</td>
</tr>
<tr>
<td>returned object record variant array RawJSON</td>
<td>返回的RawJSON值的类、记录、变体和动态数组没有正确序列化</td>
</tr>
<tr>
<td>missing or invalid value</td>
<td>返回的字符串或数值不是有效的JSON内容</td>
</tr>
</tbody>
</table>
<h5 id="toc_71">16.8.6.2.2. 返回XML内容<a class="vnote-anchor" href="#toc_71" data-anchor-icon="#"></a></h5>
<p>  默认情况下，mORMot服务端基于接口的服务将始终返回JSON数组（或JSON对象，如果<code>TServiceFactoryServer.ResultAsJSONObject</code>或<code>ResultAsJSONObjectWithoutResult</code>为<code>true</code>）。而对于某些类型的客户端（如由第三方制作的），则可能需要返回XML内容。</p>
<p>  您的mORMot服务端可以让其基于接口的服务返回XML内容，除了默认的JSON格式之外。</p>
<h6 id="toc_72">16.8.6.2.2.1. 始终返回XML内容<a class="vnote-anchor" href="#toc_72" data-anchor-icon="#"></a></h6>
<p>  如果希望给定接口的所有方法都返回XML内容而不是JSON，则可以将<code>TServiceFactoryServer.ResultAsXMLObject</code>设置为<code>true</code>。</p>
<p>  不是如下默认返回的JSON数组内容：</p>
<pre><code class="hljs">GET root/Calculator/Add?n1=1&amp;n2=2
 ...
{"result":[3]}
</code></pre>
<p>  若<code>TServiceFactoryServer.ResultAsXMLObject</code>为<code>true</code>，则返回以下XML：</p>
<pre><code class="hljs">GET root/Calculator/Add?n1=1&amp;n2=2
 ...
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;result&gt;&lt;Result&gt;3&lt;/Result&gt;&lt;/result&gt;
</code></pre>
<p>  通过调用<code>JSONBufferToXML()</code>函数对mORMot内核生成的JSON内容进行转换处理，该函数生成XML而几乎没有内存分配。 因此，可能只有轻微的性能损失（在实践中比大多数基于节点的XML生成器的速度快得多）。</p>
<p>  使用<code>TServiceFactoryServer.ResultAsXMLObject</code>属性的一个缺点是，您之前正常的Delphi或AJAX客户端将无法再使用服务，因为它们需要JSON内容。</p>
<p>  如果您希望通过XML和JSON使用您的服务，则需要定义两个服务，您可以定义一个专用接口来返回XML，然后注册该接口以仅用于返回XML：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-keyword">type</span>
  ICalculator = <span class="hljs-keyword">interface</span>(IInvokable)
    [<span class="hljs-string">'{9A60C8ED-CEB2-4E09-87D4-4A16F496E5FE}'</span>]
    <span class="hljs-comment">/// add two signed 32-bit integers</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Add</span><span class="hljs-params">(n1,n2: integer)</span>:</span> integer;
  <span class="hljs-keyword">end</span>;
  ICalculatorXML = <span class="hljs-keyword">interface</span>(ICalculator)
    [<span class="hljs-string">'{0D682D65-CE0F-441B-B4EC-2AC75E357EFE}'</span>]
  <span class="hljs-keyword">end</span>; <span class="hljs-comment">// no additional method, just new name and GUID</span>

  <span class="hljs-title">TServiceCalculator</span> = <span class="hljs-keyword">class</span>(TInterfacedObject, ICalculator,ICalculatorXML)
  <span class="hljs-keyword">public</span> <span class="hljs-comment">// implementation class should implement both interfaces</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Add</span><span class="hljs-params">(n1,n2: integer)</span>:</span> integer;
  <span class="hljs-keyword">end</span>;

...
  aServer.ServiceRegister(TServiceCalculator,[TypeInfo(ICalculator)],sicShared);
  aServer.ServiceRegister(TServiceCalculator,[TypeInfo(ICalculatorXML)],sicShared).
    ResultAsXMLObject := True;
...
</code></pre>
<p>  因此将有两个正在运行的服务实例（这里有两个<code>TServiceCalculator</code>实例，一个用于<code>ICalculator</code>，一个用于<code>ICalculatorXML</code>）。 在某些情况下，这可能会是一个问题。</p>
<p>  这样一个专用接口可能需要在服务端进行更多的测试和编码，因为它们可以从两个URI访问：</p>
<pre><code class="hljs">GET root/Calculator/Add?n1=1&amp;n2=2
 ...
{"result":{"Result":3}}
</code></pre>
<p>  而对于<code>ICalculatorXML</code>接口:</p>
<pre><code class="hljs">GET root/CalculatorXML/Add?n1=1&amp;n2=2
 ...
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;result&gt;&lt;Result&gt;3&lt;/Result&gt;&lt;/result&gt;
</code></pre>
<h6 id="toc_73">16.8.6.2.2.2. 按需返回XML内容<a class="vnote-anchor" href="#toc_73" data-anchor-icon="#"></a></h6>
<p>  作为替代方案，您可以让mORMot服务端检查传入的HTTP报文头，如果<code>"Accept: "</code>报文头是<code>"application/xml"</code>或<code>"text/xml"</code>，则将内容作为XML返回。</p>
<p>  您可以设置<code>TServiceFactoryServer.ResultAsXMLObjectIfAcceptOnlyXML</code>属性以启用此HTTP报文头检测：</p>
<pre><code class="lang-pascal hljs">  aServer.ServiceRegister(TServiceCalculator,[TypeInfo(ICalculator)],sicShared).
    ResultAsXMLObjectIfAcceptOnlyXML := true;
</code></pre>
<p>  对于标准请求，传入的HTTP报文头为<code>void</code>、<code>"Accept: */*"</code>，则返回JSON内容。</p>
<p>  如果客户端在其报文头中设置<code>"Accept: application/xml"</code>或<code>"Accept: text/xml"</code>，则它将返回XML文档。</p>
<p>  不是如下JSON内容：</p>
<pre><code class="hljs">GET root/Calculator/Add?n1=1&amp;n2=2
Accept: */*
 ...
{"result":{"Result":3}}
</code></pre>
<p>  而是返回如下XML：</p>
<pre><code class="hljs">GET root/Calculator/Add?n1=1&amp;n2=2
Accept: application/xml
 ...
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;result&gt;&lt;Result&gt;3&lt;/Result&gt;&lt;/result&gt;
</code></pre>
<p>  使用<code>"text/xml"</code>则返回：</p>
<pre><code class="hljs">GET root/Calculator/Add?n1=1&amp;n2=2
Accept: text/xml
 ...
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;result&gt;&lt;Result&gt;3&lt;/Result&gt;&lt;/result&gt;
</code></pre>
<p>  请注意，报文头应完全匹配<code>"Accept: application/xml"</code>或<code>"Accept: text/xml"</code>值。</p>
<p>  例如，若服务端未检测到<code>"Accept: text/html,application/xml,*/*"</code>，则将返回JSON：</p>
<pre><code class="hljs">GET root/Calculator/Add?n1=1&amp;n2=2
Accept: text/html,application/xml,*/*
 ...
{"result":{"Result":3}}
</code></pre>
<p>  因此，您的XML客户端应该能够强制设置准确的HTTP<code>"Accept:"</code>报文头内容。</p>
<p>  与可选的URI编码参数值一起，可用于<code>TSQLRestRoutingREST</code>默认路由方案（参见上面的<code>?n1=1&amp;n2=2</code>），它可以成为任何基于XML的客户端使用mORMot服务的有效替代方案。</p>
<h5 id="toc_74">16.8.6.2.3. 自定义返回内容<a class="vnote-anchor" href="#toc_74" data-anchor-icon="#"></a></h5>
<p>  即使响应格式默认为JSON对象，并且我们的<code>TServiceContainerClient</code>实现也需要如此，但还是有方法可以从远程请求返回任意内容。</p>
<p>  AJAX或HTML应用可以使用它来返回任意类型的数据，即不仅是JSON结果，还有纯文本、HTML甚至二进制内容。我们的<code>TServiceFactoryClient</code>实例也能够处理此类请求，并在传输一些BLOB数据时节省客户端-服务端带宽（因为它没有使用Base64编码对内容进行序列化）。</p>
<p>  为了指定自定义格式，您可以使用以下<code>TServiceCustomAnswer``record</code>类型作为接口函数的结果：</p>
<pre><code class="lang-pascal hljs">  TServiceCustomAnswer = <span class="hljs-keyword">record</span>
    Header: RawUTF8;
    Content: RawByteString;
    Status: cardinal;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  <code>Header</code>字段不应为空（即不等于<code>''</code>），包含需要的内容类型头（例如<code>TEXT_CONTENT_TYPE_HEADER</code>或<code>HTML_CONTENT_TYPE_HEADER</code>）。</p>
<p>  然后内容值将直接传回客户端，不使用JSON序列化。 当然，不会传输<code>var</code>和<code>out</code>参数（因为不再有JSON结果数组）。</p>
<p>  最后，如果默认的<code>HTTP_SUCCESS</code>不足以满足您的需要，可以使用HTML属性代码覆盖<code>Status</code>字段。 请注意，当使用Delphi客户端时，服务端应返回<code>HTTP_SUCCESS</code>：您应该仅为普通AJAX/Web客户端自定义<code>Status</code>字段。</p>
<p>  为了实现这样的方法，您可以这样定义接口：</p>
<pre><code class="lang-pascal hljs">  IComplexCalculator = <span class="hljs-keyword">interface</span>(ICalculator)
    [<span class="hljs-string">'{8D0F3839-056B-4488-A616-986CF8D4DEB7}'</span>]
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TestBlob</span><span class="hljs-params">(n: TComplexNumber)</span>:</span> TServiceCustomAnswer;
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  可以这样来实现：</p>
<pre><code class="lang-pascal hljs"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TServiceComplexCalculator</span>.<span class="hljs-title">TestBlob</span><span class="hljs-params">(n: TComplexNumber)</span>:</span> TServiceCustomAnswer;
<span class="hljs-keyword">begin</span>
  Result.Header := TEXT_CONTENT_TYPE_HEADER;
  Result.Content := FormatUTF8(<span class="hljs-string">'%,%'</span>,[n.Real,n.Imaginary]);
  <span class="hljs-comment">// leave Result.Header to its default value</span>
<span class="hljs-keyword">end</span>;
</code></pre>
<p>  这将不返回JSON对象，而是返回纯TEXT内容。</p>
<p>  回归测试将使用以下过程：</p>
<pre><code class="lang-pascal hljs">  <span class="hljs-keyword">with</span> CC.TestBlob(C3) <span class="hljs-keyword">do</span> <span class="hljs-keyword">begin</span>
    Check(Header=TEXT_CONTENT_TYPE_HEADER);
    Check(Content=FormatUTF8(<span class="hljs-string">'%,%'</span>,[C3.Real,C3.Imaginary]));
  <span class="hljs-keyword">end</span>;
</code></pre>
<p>  请注意，由于只返回了一个BLOB内容，因此不允许为此方法定义<code>var</code>和<code>out</code>参数。 否则在接口注册阶段将引发异常。 但您可以根据需要定义所需的各种<code>const</code>参数。</p>
<p>  您也可以使用此功能创建自定义的UTF-8 HTML，将<code>Header</code>值设置为<code>HTML_CONTENT_TYPE_HEADER</code>常量，并使用我们的模版快速渲染。</p>
<p>  请记住，在<code>TSQLRestRoutingJSON</code>模式下，您可以在URI级别对任何简单参数值进行编码，以传送您的浏览内容。</p>
<h2 id="toc_75">16.9. 与WCF的比较<a class="vnote-anchor" href="#toc_75" data-anchor-icon="#"></a></h2>
<p>  Microsoft <code>Windows Communication Foundation</code>是Microsoft提供的用于构建面向服务的应用程序的统一编程模型。</p>
<p>  请参阅http://msdn.microsoft.com/en-us/library/dd456779</p>
<p>  以下是WCF/mORMot SOA特征和RESTful模式实现的简短参考表。</p>
<table>
<thead>
<tr>
<th>特征</th>
<th>WCF</th>
<th>mORMot</th>
</tr>
</thead>
<tbody>
<tr>
<td>内部设计</td>
<td>集成REST的SOAP</td>
<td>RESTful</td>
</tr>
<tr>
<td>主机</td>
<td>exe/service/ISS/WAS</td>
<td>in-process/exe/service</td>
</tr>
<tr>
<td>伸缩/平衡性</td>
<td>达到WAS</td>
<td>专用主机</td>
</tr>
<tr>
<td>元数据</td>
<td>WSDL+XML</td>
<td>JSON契约</td>
</tr>
<tr>
<td>服务合同</td>
<td><code>interface</code></td>
<td><code>interface</code></td>
</tr>
<tr>
<td>数据合同</td>
<td><code>class</code></td>
<td><code>class/record</code></td>
</tr>
<tr>
<td>ORM集成</td>
<td>分离</td>
<td>与模型集成</td>
</tr>
<tr>
<td>URI定义</td>
<td>属性驱动</td>
<td>REST/JSON-RPC约定驱动或类驱动</td>
</tr>
<tr>
<td>服务契约</td>
<td><code>interface</code> + 属性</td>
<td><code>interface</code> + 共享模型</td>
</tr>
<tr>
<td>版本控制</td>
<td>XML命名空间</td>
<td><code>interface</code>签名</td>
</tr>
<tr>
<td>消息协议</td>
<td>SOAP/自定义</td>
<td>RESTful</td>
</tr>
<tr>
<td>消息传输</td>
<td>单向/双向</td>
<td>无状态（如HTTP）</td>
</tr>
<tr>
<td>序列</td>
<td>方法属性</td>
<td><code>interface</code>生命周期</td>
</tr>
<tr>
<td>事务</td>
<td>完全事务性</td>
<td>实现侧</td>
</tr>
<tr>
<td>实例生命周期</td>
<td>每次调用、每次会话、单个</td>
<td>每次调用，每次会话，每个用户，每个组，每个线程，单个，客户端驱动</td>
</tr>
<tr>
<td>配置</td>
<td><code>.config</code>文件或编码</td>
<td>约定优于配置<br>可使用代码调整</td>
</tr>
<tr>
<td>客户端访问</td>
<td>生成源层</td>
<td>没有层，直接注册</td>
</tr>
<tr>
<td>端点</td>
<td>每个合同的一个端点</td>
<td>唯一或共享的端点</td>
</tr>
<tr>
<td>操作</td>
<td>同步/异步</td>
<td>同步（REST）</td>
</tr>
<tr>
<td>会话</td>
<td>可用（可选）</td>
<td>可用（可选）</td>
</tr>
<tr>
<td>加密</td>
<td>服务级</td>
<td>通信级</td>
</tr>
<tr>
<td>压缩</td>
<td>服务级</td>
<td>通信级</td>
</tr>
<tr>
<td>序列化</td>
<td>XML/binary/JSON</td>
<td>JSON/XML/custom</td>
</tr>
<tr>
<td>通信协议</td>
<td>HTTP/HTTPS/TCP/pipe/MSMQ</td>
<td>HTTP/HTTPS/TCP/pipe/messages/in-process</td>
</tr>
<tr>
<td>HTTP/HTTPS服务</td>
<td>http.sys</td>
<td><em>http.sys</em>/native (winsock)</td>
</tr>
<tr>
<td>身份验证</td>
<td>Windows或自定义</td>
<td>Windows、基于ORM或类驱动</td>
</tr>
<tr>
<td>授权</td>
<td>通过属性或配置文件</td>
<td>每个用户组，或类驱动</td>
</tr>
<tr>
<td>线程</td>
<td>通过属性</td>
<td>在服务/方法级</td>
</tr>
<tr>
<td>程序大小</td>
<td>中等(GC, JIT, .dll)</td>
<td>小</td>
</tr>
<tr>
<td>速度</td>
<td>好</td>
<td>高</td>
</tr>
<tr>
<td>可扩展性</td>
<td>冗长但完整</td>
<td>可定制的</td>
</tr>
<tr>
<td>标准</td>
<td>事实上</td>
<td>亲和设计(如JSON, HTTP)</td>
</tr>
<tr>
<td>源代码</td>
<td>闭源</td>
<td>开源</td>
</tr>
<tr>
<td>许可</td>
<td>专用</td>
<td>开放</td>
</tr>
<tr>
<td>价格</td>
<td>视情况</td>
<td>免费</td>
</tr>
<tr>
<td>支持</td>
<td>官方 + 社区</td>
<td>Synopse + 社区</td>
</tr>
<tr>
<td>运行要求</td>
<td>.Net framework (+ISS/WAS)</td>
<td>none (新装OS即可)</td>
</tr>
</tbody>
</table>
<p>  关于实例生命周期，在WCF中<code>InstanceContextMode.Single</code>实际上与mORMot上下文中的<code>sicShared</code>相同：所有传入调用只使用一个实例，并且在调用之后不会回收。因此，<code>sicSingle</code>模式（mORMot默认）对应WCF中<code>InstanceContextMode.PerCall</code>，这意味着每次调用使用一个实例。</p>
<p>  我们可能很想说mORMot SOA架构几乎已经完成，即便是个年轻的开源项目。某些功能（如每个用户，每个组或客户端驱动的实例生命周期，或Windows消息本地通信）甚至是mORMot独有的。实际上，在实现面向服务的架构时，<code>sicClientDriven</code>非常方便。</p>
<p>  当然，WCF是基于SOAP的架构。但是WCF也受此影响：由于这种基础消息设计，它总是导致它的SOAP超重，它只是名称上的“简单”，而不是公认。</p>
<p>  如果您需要与外部服务提供者进行通信，您可以轻松地从Delphi创建SOAP网关，如下所示：</p>
<ul>
<li>导入Web服务的WSDL（Web服务定义语言）定义并将其转换为Delphi导入单元;</li>
<li>将接口发布为mORMot服务端实现类。</li>
</ul>
<p>  由于SOAP具有很多要求，并且需要根据其格式进行一些填充（特别是当服务是从C＃或Java提供的时候），我们选择不再重复发明轮子，而依赖于现有的Delphi库（在Delphi IDE可用）来实现。如果您需要跨平台的SOAP 1.1兼容解决方案，或者如果您的Delphi版本不包含SOAP进程，您可以查看<code>http://wiki.freepascal.org/Web_Service_Toolkit</code>，这是FPC、Lazarus、Delphi的Web服务包。</p>
<p>  但是对于mORMot应用域内的服务通信，RESTful/JSON方法提供了更好的性能和易用性。您不必使用WSDL或单元包装器，只需在客户端和服务端之间共享一些接口定义。一旦你使用了mORMot的<code>ServiceRegister()</code>或<code>ServiceDefine()</code>方法，你就会发现WCF管道是如何过大和过于复杂的：设想WCF只允许每个接口/契约一个端点，，这不是容易实现的方法！在SOLID设计原则的世界，接口隔离应该占统治地位。</p>
<p>  根据情况，mORMot基于接口的服务允许将其结果发布为XML，并在URI级别对输入参数进行编码。在XML世界中，这使它成为SOAP的一个很好的替代。</p>
<p>  当前，mORMot SOA唯一缺少的功能是事务处理，必须在服务端实现（如使用显式提交或回滚）、在服务器端处理。</p>

    </div>
</div>
</div>

<div id="container-floating" style="display:none;" class="d-none d-md-block d-xl-block">
    <div id="floating-button" onclick="toggleMore()">
        <p id="floating-more" class="more">&gt;</p>
    </div>
</div>

<!--
<div class="footer" id="vnote-footer">
    <p>Generated by <em><a href="https://tamlok.github.io/vnote/">VNote</a></em>.</p>
</div>
-->
</body>
</html>
